/**
*
* Copyright
* 2009-2015 Jayway Products AB
* 2016-2017 Föreningen Sambruk
*
* Licensed under AGPL, Version 3.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.gnu.org/licenses/agpl.txt
*
* 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 se.streamsource.streamflow.web.assembler;
import static org.qi4j.api.common.Visibility.application;
import static org.qi4j.api.common.Visibility.layer;
import java.util.ArrayList;
import java.util.List;
import java.util.prefs.Preferences;
import javax.sql.DataSource;
import org.qi4j.api.cache.CacheOptions;
import org.qi4j.api.common.QualifiedName;
import org.qi4j.api.common.TypeName;
import org.qi4j.api.common.Visibility;
import org.qi4j.api.entity.EntityReference;
import org.qi4j.api.entity.Identity;
import org.qi4j.api.entity.IdentityGenerator;
import org.qi4j.api.injection.scope.Structure;
import org.qi4j.api.io.Input;
import org.qi4j.api.io.Outputs;
import org.qi4j.api.io.Receiver;
import org.qi4j.api.io.Transforms;
import org.qi4j.api.service.ServiceReference;
import org.qi4j.api.specification.Specification;
import org.qi4j.api.structure.Application;
import org.qi4j.api.structure.Module;
import org.qi4j.api.unitofwork.UnitOfWork;
import org.qi4j.api.usecase.UsecaseBuilder;
import org.qi4j.api.value.ValueBuilder;
import org.qi4j.bootstrap.AssemblyException;
import org.qi4j.bootstrap.LayerAssembly;
import org.qi4j.bootstrap.ModuleAssembly;
import org.qi4j.index.reindexer.ReindexerService;
import org.qi4j.library.jmx.JMXAssembler;
import org.qi4j.spi.entity.EntityState;
import org.qi4j.spi.entitystore.EntityStore;
import org.qi4j.spi.entitystore.EntityStoreException;
import org.qi4j.spi.structure.ModuleSPI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.streamsource.infrastructure.circuitbreaker.jmx.CircuitBreakerManagement;
import se.streamsource.infrastructure.management.DatasourceConfigurationManagerService;
import se.streamsource.streamflow.api.administration.form.RequiredSignatureValue;
import se.streamsource.streamflow.api.workspace.cases.caselog.CaseLogEntryTypes;
import se.streamsource.streamflow.web.application.defaults.AvailabilityService;
import se.streamsource.streamflow.web.application.statistics.StatisticsStoreException;
import se.streamsource.streamflow.web.domain.Removable;
import se.streamsource.streamflow.web.domain.entity.caselog.CaseLogEntity;
import se.streamsource.streamflow.web.domain.entity.caze.CaseEntity;
import se.streamsource.streamflow.web.domain.entity.organization.AccessPointEntity;
import se.streamsource.streamflow.web.domain.entity.organization.OrganizationsEntity;
import se.streamsource.streamflow.web.domain.structure.caselog.CaseLog;
import se.streamsource.streamflow.web.domain.structure.caselog.CaseLogEntryValue;
import se.streamsource.streamflow.web.domain.structure.caselog.CaseLoggable;
import se.streamsource.streamflow.web.domain.structure.caze.Notes;
import se.streamsource.streamflow.web.domain.structure.conversation.Conversation;
import se.streamsource.streamflow.web.domain.structure.conversation.Message;
import se.streamsource.streamflow.web.domain.structure.conversation.Messages;
import se.streamsource.streamflow.web.domain.structure.form.DatatypeDefinition;
import se.streamsource.streamflow.web.domain.structure.form.RequiredSignatures;
import se.streamsource.streamflow.web.domain.structure.form.SelectedForms;
import se.streamsource.streamflow.web.domain.structure.note.NoteValue;
import se.streamsource.streamflow.web.domain.structure.note.NotesTimeLine;
import se.streamsource.streamflow.web.domain.structure.organization.Organization;
import se.streamsource.streamflow.web.management.CompositeMBean;
import se.streamsource.streamflow.web.management.ErrorLogService;
import se.streamsource.streamflow.web.management.EventManagerService;
import se.streamsource.streamflow.web.management.HistoryCleanup;
import se.streamsource.streamflow.web.management.InstantMessagingAdminConfiguration;
import se.streamsource.streamflow.web.management.InstantMessagingAdminService;
import se.streamsource.streamflow.web.management.ManagerComposite;
import se.streamsource.streamflow.web.management.ManagerService;
import se.streamsource.streamflow.web.management.ReindexOnStartupService;
import se.streamsource.streamflow.web.management.UpdateBuilder;
import se.streamsource.streamflow.web.management.UpdateConfiguration;
import se.streamsource.streamflow.web.management.UpdateOperation;
import se.streamsource.streamflow.web.management.UpdateService;
import se.streamsource.streamflow.web.management.jmxconnector.JmxConnectorConfiguration;
import se.streamsource.streamflow.web.management.jmxconnector.JmxConnectorService;
/**
* Assembler for management layer
*/
public class ManagementAssembler extends AbstractLayerAssembler
{
final Logger logger = LoggerFactory.getLogger( ManagementAssembler.class.getName() );
@Structure
ModuleSPI moduleSPI;
public void assemble( LayerAssembly layer ) throws AssemblyException
{
super.assemble( layer );
jmx( layer.module( "JMX" ) );
update( layer.module( "Update" ) );
}
private void jmx( ModuleAssembly module ) throws AssemblyException
{
new JMXAssembler().assemble( module );
module.objects( CompositeMBean.class );
module.transients( ManagerComposite.class );
module.services( ManagerService.class, DatasourceConfigurationManagerService.class,
ReindexOnStartupService.class, EventManagerService.class, ErrorLogService.class,
CircuitBreakerManagement.class ).visibleIn( application ).instantiateOnStartup();
module.services( ReindexerService.class ).identifiedBy( "reindexer" ).visibleIn( layer );
module.services( JmxConnectorService.class ).identifiedBy( "jmxconnector" ).instantiateOnStartup();
configuration().entities( JmxConnectorConfiguration.class ).visibleIn( Visibility.application );
configuration().forMixin( JmxConnectorConfiguration.class ).declareDefaults().enabled().set( Boolean.TRUE );
configuration().forMixin( JmxConnectorConfiguration.class ).declareDefaults().port().set( 1099 );
module.services( InstantMessagingAdminService.class ).identifiedBy( "imadmin" ).instantiateOnStartup();
configuration().entities( InstantMessagingAdminConfiguration.class ).visibleIn( Visibility.application );
configuration().forMixin( InstantMessagingAdminConfiguration.class ).declareDefaults().enabled().set( Boolean.FALSE );
}
private void update( final ModuleAssembly update )
{
UpdateBuilder updateBuilder = new UpdateBuilder( "1.4.0.0" );
updateBuilder.toVersion( "1.4.1" ).atStartup( new UpdateOperation()
{
public void update( Application app, Module module ) throws StatisticsStoreException
{
// Remove this code cause it breaks later version upgrades
}
} ).toVersion( "1.5.0.1" ).atStartup( new UpdateOperation()
{
public void update( Application app, Module module ) throws Exception
{
UnitOfWork uow = module.unitOfWorkFactory().newUnitOfWork( UsecaseBuilder.newUsecase( "AddDefaultDatatypes" ) );
try
{
OrganizationsEntity organizations = uow.get( OrganizationsEntity.class, OrganizationsEntity.ORGANIZATIONS_ID );
Organization organization = organizations.organization().get();
DatatypeDefinition newDatatype = organization
.createDatatypeDefinition( "http://www.w3.org/2006/vcard/ns#Email" );
newDatatype.changeDescription( "Epost" );
newDatatype.changeRegularExpression( "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}$" );
newDatatype = organization.createDatatypeDefinition( "http://www.w3.org/2006/vcard/ns#geo" );
newDatatype.changeDescription( "Kartkoordinat" );
newDatatype.changeRegularExpression( "\\d{5}\\.\\d{3},\\d{5}\\.\\d{3}" );
newDatatype = organization.createDatatypeDefinition( "http://www.w3.org/2006/vcard/ns#fn" );
newDatatype.changeDescription( "Namn" );
newDatatype = organization.createDatatypeDefinition( "http://www.w3.org/2006/vcard/ns#street-address" );
newDatatype.changeDescription( "Gatuadress" );
newDatatype = organization.createDatatypeDefinition( "http://www.w3.org/2006/vcard/ns#postal-code" );
newDatatype.changeDescription( "Postnummer" );
newDatatype = organization.createDatatypeDefinition( "http://www.w3.org/2006/vcard/ns#locality" );
newDatatype.changeDescription( "Postort" );
newDatatype = organization.createDatatypeDefinition( "http://www.w3.org/2006/vcard/ns#tel" );
newDatatype.changeDescription( "Telefon" );
newDatatype = organization.createDatatypeDefinition( "http://www.w3.org/2006/vcard/ns#Cell" );
newDatatype.changeDescription( "Mobilnummer" );
uow.complete();
} finally
{
uow.discard();
}
}
} ).toVersion( "1.5.0.2" ).atStartup( new UpdateOperation()
{
public void update( Application app, Module module ) throws Exception
{
// reindex rdf and solr indexes since this version contains two solr core's.
ManagerService mgrService = (ManagerService) module.serviceFinder().findService( ManagerService.class ).get();
if (mgrService != null)
mgrService.getManager().reindex();
// DataSourceConfiguration has moved to SPI and java prefs have to reflect the structural change
if (Preferences.userRoot().nodeExists( "/streamsource/streamflow/StreamflowServer/streamflowds" ))
{
Preferences preference = Preferences.userRoot().node( "/streamsource/streamflow/StreamflowServer/streamflowds" );
preference.put( "type", "se.streamsource.infrastructure.database.DataSourceConfiguration" );
preference.flush();
}
}
} ).toVersion( "1.6.0.0" ).atStartup( new UpdateOperation()
{
public void update( Application app, final Module module ) throws Exception
{
// For each case create a new Notes association, create a NoteValue, put it into the notes list and delete the note from Notable.
// Fetch a list of relevant case id's first and work with the list.
// JDBM is not happy about committing stuff to the database
// while traversing the index. ( results in random EOFExceptions!! )
int count = 0;
final List<String> caseIds = new ArrayList<String>();
try
{
Input<EntityState, EntityStoreException> entities = ((EntityStore) module.serviceFinder().findService( EntityStore.class ).get()).entityStates( (ModuleSPI) module );
entities.transferTo( Transforms.filter( new Specification<EntityState>()
{
public boolean satisfiedBy( EntityState state )
{
return state.isOfType( TypeName.nameOf( CaseEntity.class ) ) &&
(state.getAssociation( QualifiedName.fromClass( Notes.Data.class, "notes" ) ) == null
|| state.getAssociation( QualifiedName.fromClass( CaseLoggable.Data.class, "caselog" ) ) == null);
}
}, Outputs.withReceiver( new Receiver<EntityState, Throwable>()
{
public void receive( EntityState state ) throws Throwable
{
caseIds.add( state.identity().identity() );
}
} ) ) );
logger.info( "Found " + caseIds.size() + " cases eligible for update migration." );
UnitOfWork uow = null;
ServiceReference<IdentityGenerator> identityGenerator = module.serviceFinder().findService( IdentityGenerator.class );
for( String id : caseIds )
{
try
{
if (uow == null)
{
uow = module.unitOfWorkFactory().newUnitOfWork( UsecaseBuilder.buildUsecase( "Upgrade_1.6.0.0" ).with( CacheOptions.NEVER ).newUsecase( ) );
}
CaseEntity caze = uow.get( CaseEntity.class, id );
if (caze.notes().get() == null)
{
// Create list of Notes
NotesTimeLine notesEntity = module.unitOfWorkFactory().currentUnitOfWork().newEntity( NotesTimeLine.class, identityGenerator.get().generate( Identity.class ) );
caze.notes().set( notesEntity );
ValueBuilder<NoteValue> noteValueBuilder = module.valueBuilderFactory().newValueBuilder( NoteValue.class );
noteValueBuilder.prototype().note().set( caze.note().get() );
noteValueBuilder.prototype().createdBy().set( EntityReference.getEntityReference( caze.createdBy().get() ) );
noteValueBuilder.prototype().createdOn().set( caze.createdOn().get() );
((NotesTimeLine.Data) caze.notes().get()).notes().get().add( noteValueBuilder.newInstance() );
caze.note().set( "" );
}
if (caze.caselog().get() == null)
{
// Transform History to CaseLog
CaseLogEntity caseLog = module.unitOfWorkFactory().currentUnitOfWork().newEntity( CaseLogEntity.class, identityGenerator.get().generate( Identity.class ) );
caze.caselog().set( caseLog );
Conversation history = caze.history().get();
if (history != null)
{
for (Message message : ((Messages.Data) history).messages())
{
Message.Data messageData = (Message.Data) message;
ValueBuilder<CaseLogEntryValue> builder = module.valueBuilderFactory().newValueBuilder(
CaseLogEntryValue.class );
builder.prototype().createdBy()
.set( EntityReference.getEntityReference( messageData.sender().get() ) );
builder.prototype().createdOn().set( messageData.createdOn().get() );
if (messageData.body().get() != null && messageData.body().get().startsWith( "{" ))
{
builder.prototype().entryType().set( CaseLogEntryTypes.system );
} else
{
builder.prototype().entryType().set( CaseLogEntryTypes.custom );
}
builder.prototype().message().set( messageData.body().get() );
((CaseLog.Data) caze.caselog().get()).addedEntry( null, builder.newInstance() );
}
}
}
count++;
if (count % 1000 == 0)
{
logger.info( " " + count + " cases notes and/or caselog migrated and about to commit" );
uow.complete();
uow = null;
logger.info( "Commit succeded." );
}
} catch (Throwable e)
{
uow.discard();
logger.error( e.getMessage() );
throw new RuntimeException( "Upgrade failed at case count " + count + " !", e );
}
}
// only try to commit if Outputs was not empty set
// if we haven't received anything the uow will be null!
if (uow != null)
uow.complete();
logger.info( "Upgrade migration for 1.6.0.0 migrated " + count + " cases successfully." );
// now we may open up for client trafik again - set the circuitbreaker to on
// database is migrated and history was dereferenced before deleting.
AvailabilityService availablilityService = (AvailabilityService) module.serviceFinder().findService( AvailabilityService.class ).get();
availablilityService.getCircuitBreaker().turnOn();
// Run refresh statistics only if we found any case's to migrate
if (caseIds.size() > 0)
{
ManagerService mgrService = (ManagerService) module.serviceFinder().findService( ManagerService.class ).get();
ServiceReference<DataSource> dataSource = module.serviceFinder().findService( DataSource.class );
try
{
if (dataSource != null && dataSource.isActive())
mgrService.getManager().refreshStatistics();
else
logger.info( "Could not refresh statistics, DataSource streamflowds is not active!" );
} catch (StatisticsStoreException e)
{
logger.info( "Could not refresh statistics", e );
}
}
} catch (Throwable e)
{
logger.error( e.getMessage() );
throw new RuntimeException( "Upgrade failed at case count " + count + " !", e );
}
}
} ).toVersion( "1.8.0.0" ).atStartup( new UpdateOperation()
{
public void update( Application app, Module module ) throws Exception
{
if (Preferences.userRoot().nodeExists( "/streamsource/streamflow/StreamflowServer/contactlookup" ))
{
Preferences preference = Preferences.userRoot().node( "/streamsource/streamflow/StreamflowServer/contactlookup" );
preference.put( "type", "se.streamsource.streamflow.web.infrastructure.plugin.ContactLookupServiceConfiguration" );
preference.flush();
}
int count = 0;
final List<String> accessPointIds = new ArrayList<String>();
try
{
Input<EntityState, EntityStoreException> entities = ((EntityStore) module.serviceFinder().findService( EntityStore.class ).get()).entityStates( (ModuleSPI) module );
entities.transferTo( Transforms.filter( new Specification<EntityState>()
{
public boolean satisfiedBy( EntityState state )
{
return state.isOfType( TypeName.nameOf( AccessPointEntity.class ) ) &&
(state.getManyAssociation( QualifiedName.fromClass( SelectedForms.Data.class, "selectedForms" ) ).count() > 0
&& !(Boolean)state.getProperty( QualifiedName.fromClass( Removable.Data.class, "removed" ) ) );
}
}, Outputs.withReceiver( new Receiver<EntityState, Throwable>()
{
public void receive( EntityState state ) throws Throwable
{
accessPointIds.add( state.identity().identity() );
}
} ) ) );
logger.info( "Found " + accessPointIds.size() + " access points eligible for update migration." );
UnitOfWork uow = null;
for( String id : accessPointIds )
{
try
{
if (uow == null)
{
uow = module.unitOfWorkFactory().newUnitOfWork( UsecaseBuilder.buildUsecase( "Upgrade_1.8.0.0" ).with( CacheOptions.NEVER ).newUsecase( ) );
}
AccessPointEntity accessPointEntity = uow.get( AccessPointEntity.class, id );
List<RequiredSignatureValue> signatureList = ((RequiredSignatures.Data) ((SelectedForms.Data) accessPointEntity).selectedForms().get( 0 )).requiredSignatures().get();
if( signatureList != null && !signatureList.isEmpty() && ((RequiredSignatures.Data)accessPointEntity).requiredSignatures().get().isEmpty() )
{
// Found a suitable signature on existing form and no signature present on access point - move it to access point instead.
RequiredSignatureValue signatureValue = signatureList.get( 0 );
ValueBuilder<RequiredSignatureValue> signatureBuilder = signatureValue.buildWith();
signatureBuilder.prototype().active().set( Boolean.TRUE );
signatureBuilder.prototype().mandatory().set( Boolean.TRUE );
signatureBuilder.prototype().formid().set( ((Identity)accessPointEntity.selectedForms().get( 0 )).identity().get() );
signatureBuilder.prototype().formdescription().set( accessPointEntity.selectedForms().get( 0 ).getDescription() );
accessPointEntity.createRequiredSignature( signatureBuilder.newInstance() );
logger.info( "Upgrade migration for 1.8.0.0 actually moved a signature to access point: " + id );
}
count++;
} catch (Throwable e)
{
uow.discard();
logger.error( e.getMessage() );
throw new RuntimeException( "Upgrade failed at access point count " + count + " !", e );
}
}
if (uow != null)
uow.complete();
logger.info( "Upgrade migration for 1.8.0.0 migrated " + count + " access points successfully." );
}
catch (Throwable e)
{
logger.error( e.getMessage() );
throw new RuntimeException( "Upgrade failed at access point count " + count + " !", e );
}
}
} );
update.services( UpdateService.class ).identifiedBy( "update" ).setMetaInfo( updateBuilder )
.visibleIn( layer ).instantiateOnStartup();
update.objects( HistoryCleanup.class );
configuration().entities( UpdateConfiguration.class ).visibleIn( application );
// default value for first installation has to be the same version as the UpdateBuilder start version.
configuration().forMixin( UpdateConfiguration.class ).declareDefaults().lastStartupVersion().set( "1.4.0.0" );
}
}