| /* |
| * Copyright (c) 2009-2011, Rickard Öberg. All Rights Reserved. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
| * implied. |
| * |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.qi4j.migration; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import org.json.JSONArray; |
| import org.json.JSONException; |
| import org.json.JSONObject; |
| import org.qi4j.api.activation.ActivatorAdapter; |
| import org.qi4j.api.activation.Activators; |
| import org.qi4j.api.configuration.Configuration; |
| import org.qi4j.api.injection.scope.Service; |
| import org.qi4j.api.injection.scope.Structure; |
| import org.qi4j.api.injection.scope.This; |
| import org.qi4j.api.injection.scope.Uses; |
| import org.qi4j.api.mixin.Mixins; |
| import org.qi4j.api.service.ServiceDescriptor; |
| import org.qi4j.api.service.ServiceReference; |
| import org.qi4j.api.structure.Application; |
| import org.qi4j.api.unitofwork.UnitOfWorkFactory; |
| import org.qi4j.migration.assembly.EntityMigrationRule; |
| import org.qi4j.migration.assembly.MigrationBuilder; |
| import org.qi4j.migration.assembly.MigrationRule; |
| import org.qi4j.spi.entitystore.EntityStore; |
| import org.qi4j.spi.entitystore.helpers.JSONKeys; |
| import org.qi4j.spi.entitystore.helpers.Migration; |
| import org.qi4j.spi.entitystore.helpers.StateStore; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Migration service. This is used by MapEntityStore EntityStore implementations to |
| * migrate JSON state for Entities. To use it register the service so that the EntityStore |
| * can access it, and then create MigrationRules during the assembly of your application, |
| * which is registered as metainfo for this service. |
| * |
| * The rules invoke operations, which in turn invoke the Migrator interface to perform the actual |
| * migration. If the Migrator accepts a migration command the change is performed, and an event |
| * is triggered. These events can be received by implementing the MigrationEvents interface in a service |
| * that is visible by this MigrationService. |
| */ |
| @Mixins( MigrationService.MigrationMixin.class ) |
| @Activators( MigrationService.Activator.class ) |
| public interface MigrationService |
| extends Migration |
| { |
| |
| void initialize() |
| throws Exception; |
| |
| class Activator |
| extends ActivatorAdapter<ServiceReference<MigrationService>> |
| { |
| |
| @Override |
| public void afterActivation( ServiceReference<MigrationService> activated ) |
| throws Exception |
| { |
| activated.get().initialize(); |
| } |
| |
| } |
| |
| public class MigrationMixin |
| implements MigrationService, Migrator |
| { |
| @Structure |
| Application app; |
| |
| @This |
| Configuration<MigrationConfiguration> config; |
| |
| @Uses |
| ServiceDescriptor descriptor; |
| |
| @Service |
| StateStore store; |
| @Service |
| EntityStore entityStore; |
| @Structure |
| UnitOfWorkFactory uowf; |
| |
| @This |
| Migrator migrator; |
| |
| public MigrationBuilder builder; |
| public Logger log; |
| |
| @Service |
| Iterable<MigrationEvents> migrationEvents; |
| |
| @Override |
| public boolean migrate( JSONObject state, String toVersion, StateStore stateStore ) |
| throws JSONException |
| { |
| // Get current version |
| String fromVersion = state.optString( JSONKeys.APPLICATION_VERSION, "0.0" ); |
| |
| Iterable<EntityMigrationRule> matchedRules = builder.entityMigrationRules().rulesBetweenVersions( fromVersion, toVersion ); |
| |
| boolean changed = false; |
| if( matchedRules != null ) |
| { |
| for( EntityMigrationRule matchedRule : matchedRules ) |
| { |
| boolean ruleExecuted = matchedRule.upgrade( state, stateStore, migrator ); |
| |
| if( ruleExecuted && log.isDebugEnabled() ) |
| { |
| log.debug( matchedRule.toString() ); |
| } |
| |
| changed = ruleExecuted || changed; |
| } |
| } |
| |
| state.put( JSONKeys.APPLICATION_VERSION, toVersion ); |
| |
| if( changed ) |
| { |
| log.info( "Migrated " + state.getString( JSONKeys.IDENTITY ) + " from " + fromVersion + " to " + toVersion ); |
| } |
| |
| return changed; |
| } |
| |
| @Override |
| public void initialize() |
| throws Exception |
| { |
| builder = descriptor.metaInfo( MigrationBuilder.class ); |
| |
| log = LoggerFactory.getLogger( MigrationService.class ); |
| |
| String version = app.version(); |
| String lastVersion = config.get().lastStartupVersion().get(); |
| |
| // Run general rules if version has changed |
| if( !app.version().equals( lastVersion ) ) |
| { |
| Iterable<MigrationRule> rules = builder.migrationRules().rulesBetweenVersions( lastVersion, version ); |
| List<MigrationRule> executedRules = new ArrayList<>(); |
| try |
| { |
| if( rules != null ) |
| { |
| for( MigrationRule rule : rules ) |
| { |
| rule.upgrade( store, this ); |
| executedRules.add( rule ); |
| log.debug( rule.toString() ); |
| } |
| |
| log.info( "Migrated to " + version ); |
| } |
| |
| config.get().lastStartupVersion().set( version ); |
| config.save(); |
| } |
| catch( Exception e ) |
| { |
| log.error( "Upgrade failed", e ); |
| |
| // Downgrade the migrated rules |
| for( MigrationRule executedRule : executedRules ) |
| { |
| executedRule.downgrade( store, this ); |
| } |
| } |
| } |
| } |
| |
| // Migrator implementation |
| @Override |
| public boolean addProperty( JSONObject state, String name, Object defaultValue ) |
| throws JSONException |
| { |
| JSONObject properties = state.getJSONObject( JSONKeys.PROPERTIES ); |
| if( !properties.has( name ) ) |
| { |
| if( defaultValue == null ) |
| { |
| properties.put( name, JSONObject.NULL ); |
| } |
| else |
| { |
| properties.put( name, defaultValue ); |
| } |
| |
| for( MigrationEvents migrationEvent : migrationEvents ) |
| { |
| migrationEvent.propertyAdded( state.getString( JSONKeys.IDENTITY ), name, defaultValue ); |
| } |
| |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean removeProperty( JSONObject state, String name ) |
| throws JSONException |
| { |
| JSONObject properties = state.getJSONObject( JSONKeys.PROPERTIES ); |
| if( properties.has( name ) ) |
| { |
| properties.remove( name ); |
| for( MigrationEvents migrationEvent : migrationEvents ) |
| { |
| migrationEvent.propertyRemoved( state.getString( JSONKeys.IDENTITY ), name ); |
| } |
| |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean renameProperty( JSONObject state, String from, String to ) |
| throws JSONException |
| { |
| JSONObject properties = state.getJSONObject( JSONKeys.PROPERTIES ); |
| if( properties.has( from ) ) |
| { |
| Object value = properties.remove( from ); |
| properties.put( to, value ); |
| for( MigrationEvents migrationEvent : migrationEvents ) |
| { |
| migrationEvent.propertyRenamed( state.getString( JSONKeys.IDENTITY ), from, to ); |
| } |
| |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean addAssociation( JSONObject state, String name, String defaultReference ) |
| throws JSONException |
| { |
| JSONObject associations = state.getJSONObject( JSONKeys.ASSOCIATIONS ); |
| if( !associations.has( name ) ) |
| { |
| if( defaultReference == null ) |
| { |
| associations.put( name, JSONObject.NULL ); |
| } |
| else |
| { |
| associations.put( name, defaultReference ); |
| } |
| |
| for( MigrationEvents migrationEvent : migrationEvents ) |
| { |
| migrationEvent.associationAdded( state.getString( JSONKeys.IDENTITY ), name, defaultReference ); |
| } |
| |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean removeAssociation( JSONObject state, String name ) |
| throws JSONException |
| { |
| JSONObject associations = state.getJSONObject( JSONKeys.ASSOCIATIONS ); |
| if( associations.has( name ) ) |
| { |
| associations.remove( name ); |
| for( MigrationEvents migrationEvent : migrationEvents ) |
| { |
| migrationEvent.associationRemoved( state.getString( JSONKeys.IDENTITY ), name ); |
| } |
| |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean renameAssociation( JSONObject state, String from, String to ) |
| throws JSONException |
| { |
| JSONObject associations = state.getJSONObject( JSONKeys.ASSOCIATIONS ); |
| if( associations.has( from ) ) |
| { |
| Object value = associations.remove( from ); |
| associations.put( to, value ); |
| |
| for( MigrationEvents migrationEvent : migrationEvents ) |
| { |
| migrationEvent.associationRenamed( state.getString( JSONKeys.IDENTITY ), from, to ); |
| } |
| |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean addManyAssociation( JSONObject state, String name, String... defaultReferences ) |
| throws JSONException |
| { |
| JSONObject manyAssociations = state.getJSONObject( JSONKeys.MANY_ASSOCIATIONS ); |
| if( !manyAssociations.has( name ) ) |
| { |
| JSONArray references = new JSONArray(); |
| for( String reference : defaultReferences ) |
| { |
| references.put( reference ); |
| } |
| manyAssociations.put( name, references ); |
| |
| for( MigrationEvents migrationEvent : migrationEvents ) |
| { |
| migrationEvent.manyAssociationAdded( state.getString( JSONKeys.IDENTITY ), name, defaultReferences ); |
| } |
| |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean removeManyAssociation( JSONObject state, String name ) |
| throws JSONException |
| { |
| JSONObject manyAssociations = state.getJSONObject( JSONKeys.MANY_ASSOCIATIONS ); |
| if( manyAssociations.has( name ) ) |
| { |
| manyAssociations.remove( name ); |
| for( MigrationEvents migrationEvent : migrationEvents ) |
| { |
| migrationEvent.manyAssociationRemoved( state.getString( JSONKeys.IDENTITY ), name ); |
| } |
| |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean renameManyAssociation( JSONObject state, String from, String to ) |
| throws JSONException |
| { |
| JSONObject manyAssociations = state.getJSONObject( JSONKeys.MANY_ASSOCIATIONS ); |
| if( manyAssociations.has( from ) ) |
| { |
| Object value = manyAssociations.remove( from ); |
| manyAssociations.put( to, value ); |
| |
| for( MigrationEvents migrationEvent : migrationEvents ) |
| { |
| migrationEvent.manyAssociationRenamed( state.getString( JSONKeys.IDENTITY ), from, to ); |
| } |
| |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean addNamedAssociation( JSONObject state, String name, Map<String, String> defaultReferences ) |
| throws JSONException |
| { |
| JSONObject namedAssociations = state.getJSONObject( JSONKeys.NAMED_ASSOCIATIONS ); |
| if( !namedAssociations.has( name ) ) |
| { |
| JSONObject references = new JSONObject(); |
| for( Map.Entry<String, String> namedRef : defaultReferences.entrySet() ) |
| { |
| references.put( namedRef.getKey(), namedRef.getValue() ); |
| } |
| namedAssociations.put( name, references ); |
| |
| for( MigrationEvents migrationEvent : migrationEvents ) |
| { |
| migrationEvent.namedAssociationAdded( state.getString( JSONKeys.IDENTITY ), name, defaultReferences ); |
| } |
| |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean removeNamedAssociation( JSONObject state, String name ) |
| throws JSONException |
| { |
| JSONObject namedAssociations = state.getJSONObject( JSONKeys.NAMED_ASSOCIATIONS ); |
| if( namedAssociations.has( name ) ) |
| { |
| namedAssociations.remove( name ); |
| |
| for( MigrationEvents migrationEvent : migrationEvents ) |
| { |
| migrationEvent.namedAssociationRemoved( state.getString( JSONKeys.IDENTITY ), name ); |
| } |
| |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean renameNamedAssociation( JSONObject state, String from, String to ) |
| throws JSONException |
| { |
| JSONObject namedAssociations = state.getJSONObject( JSONKeys.NAMED_ASSOCIATIONS ); |
| if( namedAssociations.has( from ) ) |
| { |
| Object value = namedAssociations.remove( from ); |
| namedAssociations.put( to, value ); |
| |
| for( MigrationEvents migrationEvent : migrationEvents ) |
| { |
| migrationEvent.namedAssociationRenamed( state.getString( JSONKeys.IDENTITY ), from, to ); |
| } |
| |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| @Override |
| public void changeEntityType( JSONObject state, String newEntityType ) |
| throws JSONException |
| { |
| state.put( JSONKeys.TYPE, newEntityType ); |
| |
| for( MigrationEvents migrationEvent : migrationEvents ) |
| { |
| migrationEvent.entityTypeChanged( state.getString( JSONKeys.IDENTITY ), newEntityType ); |
| } |
| } |
| } |
| |
| } |