package org.activityinfo.server.command.handler;
/*
* #%L
* ActivityInfo Server
* %%
* Copyright (C) 2009 - 2013 UNICEF
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import com.google.api.client.util.Lists;
import com.google.api.client.util.Maps;
import com.google.api.client.util.Strings;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;
import com.google.inject.Injector;
import org.activityinfo.legacy.shared.command.CloneDatabase;
import org.activityinfo.legacy.shared.command.GetFormClass;
import org.activityinfo.legacy.shared.command.UpdateFormClass;
import org.activityinfo.legacy.shared.command.result.CreateResult;
import org.activityinfo.legacy.shared.command.result.FormClassResult;
import org.activityinfo.legacy.shared.command.result.VoidResult;
import org.activityinfo.legacy.shared.exception.IllegalAccessCommandException;
import org.activityinfo.legacy.shared.impl.CommandHandlerAsync;
import org.activityinfo.legacy.shared.impl.ExecutionContext;
import org.activityinfo.model.form.*;
import org.activityinfo.model.legacy.CuidAdapter;
import org.activityinfo.model.legacy.KeyGenerator;
import org.activityinfo.model.resource.ResourceId;
import org.activityinfo.model.type.FieldType;
import org.activityinfo.model.type.ParametrizedFieldType;
import org.activityinfo.model.type.ReferenceType;
import org.activityinfo.model.type.enumerated.EnumItem;
import org.activityinfo.model.type.enumerated.EnumType;
import org.activityinfo.model.type.expr.CalculatedFieldType;
import org.activityinfo.model.type.number.QuantityType;
import org.activityinfo.model.type.time.LocalDateType;
import org.activityinfo.promise.Promise;
import org.activityinfo.server.database.hibernate.entity.*;
import org.activityinfo.server.endpoint.gwtrpc.RemoteExecutionContext;
import javax.persistence.EntityManager;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.activityinfo.model.legacy.CuidAdapter.BUILTIN_FIELDS;
/**
* @author yuriyz on 11/17/2014.
*/
public class CloneDatabaseHandler implements CommandHandlerAsync<CloneDatabase, CreateResult> {
private static final Logger LOGGER = Logger.getLogger(CloneDatabaseHandler.class.getName());
private final EntityManager em;
private final PermissionOracle permissionOracle;
private final KeyGenerator generator = new KeyGenerator();
// Mappings old id (source db) -> new id (target/newly created db)
private final Map<Integer, Partner> partnerMapping = Maps.newHashMap();
private final Map<Integer, Activity> activityMapping = Maps.newHashMap();
private final Map<Integer, AttributeGroup> attributeGroupMapping = Maps.newHashMap();
private CloneDatabase command;
private UserDatabase targetDb;
private UserDatabase sourceDb;
@Inject
public CloneDatabaseHandler(Injector injector) {
this.em = injector.getInstance(EntityManager.class);
this.permissionOracle = injector.getInstance(PermissionOracle.class);
}
@Override
public void execute(CloneDatabase command, ExecutionContext context, final AsyncCallback<CreateResult> callback) {
final User user = ((RemoteExecutionContext) context).retrieveUserEntity();
this.command = command;
this.targetDb = createDatabase(command, user);
this.sourceDb = em.find(UserDatabase.class, command.getSourceDatabaseId());
if (!permissionOracle.isViewAllowed(sourceDb, user)) {
throw new IllegalAccessCommandException();
}
// 1. copy partners and keep mapping between old and new partners
if (command.isCopyPartners() || command.isCopyUserPermissions()) {
copyPartners();
}
// 2. copy user permissions : without design privileges the user shouldn't be able to see the list of users.
if (command.isCopyUserPermissions() && permissionOracle.isDesignAllowed(sourceDb, user)) {
copyUserPermissions();
}
List<Promise<Void>> promises = new ArrayList<>();
// 3. copy forms and form data
promises.add(copyFormData(context));
Promise.waitAll(promises).then(new AsyncCallback<Void>() {
@Override
public void onFailure(Throwable caught) {
callback.onFailure(caught);
}
@Override
public void onSuccess(Void result) {
callback.onSuccess(new CreateResult(targetDb.getId()));
}
});
}
private void copyUserPermissions() {
for (UserPermission sourcePermission : sourceDb.getUserPermissions()) {
UserPermission newPermission = new UserPermission(sourcePermission);
newPermission.setDatabase(targetDb);
newPermission.setLastSchemaUpdate(new Date());
// set newly created partner
if (sourcePermission.getPartner() != null) {
Partner targetPartner = partnerMapping.get(sourcePermission.getPartner().getId());
newPermission.setPartner(targetPartner != null ? targetPartner : null);
}
em.persist(newPermission);
}
}
private void copyPartners() {
for (Partner partner : sourceDb.getPartners()) {
Partner newPartner = new Partner();
newPartner.setName(partner.getName());
newPartner.setFullName(partner.getFullName());
em.persist(newPartner);
partnerMapping.put(partner.getId(), newPartner);
targetDb.getPartners().add(newPartner);
}
targetDb.setLastSchemaUpdate(new Date());
em.persist(targetDb);
}
private Promise<Void> copyFormData(final ExecutionContext context) {
// first copy all activities without payload (indicators, attributes)
for (Activity activity : sourceDb.getActivities()) {
copyActivity(activity);
}
final List<Promise<VoidResult>> copyPromises = new ArrayList<>();
for (Activity activity : sourceDb.getActivities()) {
final ResourceId sourceFormClass = CuidAdapter.activityFormClass(activity.getId());
final ResourceId targetFormClass = CuidAdapter.activityFormClass(activityMapping.get(activity.getId()).getId());
// copy form class
copyPromises.add(copyFormClass(context, sourceFormClass, targetFormClass));
// if the new countryId of the target database is different than the countryId of sourceDatabase,
// copyData must be false -> skip data copy
if (command.isCopyData() && sourceDb.getCountry().getId() == targetDb.getCountry().getId()) {
// site form instances
// todo : commenting it temporary until we have nice idea how to implement it in scalable manner. (AI-787)
// copyPromises.add(copySiteFormInstances(context, activity, newActivity));
}
}
return Promise.waitAll(copyPromises);
}
// private Promise<VoidResult> copySiteFormInstances(final ExecutionContext context, Activity sourceActivity, final Activity targetActivity) {
// Filter filter = new Filter();
// filter.addRestriction(DimensionType.Activity, sourceActivity.getId());
//
// GetSites query = new GetSites();
// query.setFilter(filter);
//
// final Promise<ActivityFormDTO> activityForm = new Promise<>();
// context.execute(new GetActivityForm(sourceActivity.getId()), activityForm);
//
// final Promise<SiteResult> fetchSitesPromise = new Promise<>();
// context.execute(query, fetchSitesPromise);
//
// return Promise.waitAll(activityForm, fetchSitesPromise).join(new Function<Void, Promise<VoidResult>>() {
// @Nullable
// @Override
// public Promise<VoidResult> apply(@Nullable Void input) {
//
// for (SiteDTO site : fetchSitesPromise.get().getData()) {
// SiteBinding binding = new SiteBinding(activityForm.get());
//
// // adapt id and classId to targetActivity
// FormInstance formInstance = binding.newInstance(site)
// .setId(CuidAdapter.cuid(CuidAdapter.SITE_DOMAIN, targetActivity.getId()))
// .setClassId(CuidAdapter.activityFormClass(targetActivity.getId()));
//
// // persist
// new SitePersister(new DispatchAdapter(context)).persist(formInstance);
// }
// return Promise.resolved(VoidResult.INSTANCE);
// }
// });
// }
private Promise<VoidResult> copyFormClass(final ExecutionContext context, final ResourceId sourceFormClassId, final ResourceId targetFormClassId) {
final Promise<VoidResult> promise = new Promise<>();
context.execute(new GetFormClass(sourceFormClassId), new AsyncCallback<FormClassResult>() {
@Override
public void onFailure(Throwable caught) {
LOGGER.log(Level.SEVERE, caught.getMessage(), caught);
promise.onFailure(caught);
}
@Override
public void onSuccess(FormClassResult sourceFormClass) {
FormClass targetFormClass = cloneFormClass(sourceFormClass.getFormClass(), new FormClass(targetFormClassId));
context.execute(new UpdateFormClass(targetFormClass), promise);
}
});
return promise;
}
private FormClass cloneFormClass(FormClass sourceFormClass, FormClass targetFormClass) {
targetFormClass.setLabel(sourceFormClass.getLabel());
targetFormClass.setDescription(sourceFormClass.getDescription());
targetFormClass.setParentId(CuidAdapter.databaseId(targetDb.getId()));
copyFormElements(sourceFormClass, targetFormClass, sourceFormClass.getId(), targetFormClass.getId());
return targetFormClass;
}
private void copyFormElements(FormElementContainer sourceContainer, FormElementContainer targetContainer, ResourceId sourceClassId, ResourceId targetClassId) {
for (FormElement element : sourceContainer.getElements()) {
if (element instanceof FormSection) {
FormSection sourceSection = (FormSection) element;
FormSection targetSection = new FormSection(ResourceId.generateId());
targetSection.setLabel(sourceSection.getLabel());
targetContainer.addElement(targetSection);
copyFormElements(sourceSection, targetSection, sourceClassId, targetClassId);
} else if (element instanceof FormField) {
FormField sourceField = (FormField) element;
FormField targetField = new FormField(targetFieldId(sourceField, sourceClassId, targetClassId));
targetField.setType(targetFieldType(sourceField));
targetField.setCode(sourceField.getCode());
targetField.setRelevanceConditionExpression(sourceField.getRelevanceConditionExpression());
targetField.setLabel(sourceField.getLabel());
targetField.setDescription(sourceField.getDescription());
targetField.setReadOnly(sourceField.isReadOnly());
targetField.setRequired(sourceField.isRequired());
targetField.setSuperProperties(sourceField.getSuperProperties());
targetContainer.addElement(targetField);
} else {
throw new RuntimeException("Unsupported FormElement : " + element);
}
}
}
private FieldType targetFieldType(FormField sourceField) {
FieldType fieldType = sourceField.getType();
if (!(fieldType instanceof ParametrizedFieldType)) {
return fieldType;
}
if (fieldType instanceof QuantityType ||
fieldType instanceof CalculatedFieldType ||
fieldType instanceof LocalDateType) {
return fieldType;
}
if (fieldType instanceof EnumType) {
if (sourceField.getId().getDomain() == CuidAdapter.ATTRIBUTE_GROUP_FIELD_DOMAIN) {
EnumType sourceEnumType = (EnumType) fieldType;
List<EnumItem> targetValues = Lists.newArrayList();
for (EnumItem sourceValue : sourceEnumType.getValues()) {
ResourceId targetValueId = CuidAdapter.cuid(sourceValue.getId().getDomain(), generator.generateInt());
targetValues.add(new EnumItem(targetValueId, sourceValue.getLabel()));
}
return new EnumType(sourceEnumType.getCardinality(), targetValues);
}
}
if (fieldType instanceof ReferenceType) {
ReferenceType sourceType = (ReferenceType) fieldType;
Set<ResourceId> sourceRange = sourceType.getRange();
Set<ResourceId> targetRange = new HashSet<>();
switch (sourceRange.iterator().next().getDomain()) {
case CuidAdapter.PARTNER_FORM_CLASS_DOMAIN:
if (command.isCopyPartners()) {
for (ResourceId item : sourceRange) {
Partner targetPartner = partnerMapping.get(CuidAdapter.getLegacyIdFromCuid(item));
targetRange.add(CuidAdapter.partnerFormClass(targetPartner.getId()));
}
}
break;
}
// fallback to source targetRange
if (targetRange.isEmpty()) {
targetRange = sourceRange;
}
return new ReferenceType()
.setCardinality(sourceType.getCardinality())
.setRange(targetRange);
}
throw new RuntimeException("Unable to generate field id for fieldType : " + fieldType);
}
private ResourceId targetFieldId(FormField sourceField, ResourceId sourceClassId, ResourceId targetClassId) {
ResourceId sourceFieldId = sourceField.getId();
for (int fieldIndex : BUILTIN_FIELDS) {
if (sourceFieldId.equals(CuidAdapter.field(sourceClassId, fieldIndex))) {
return CuidAdapter.field(targetClassId, fieldIndex);
}
}
return CuidAdapter.cuid(sourceField.getId().getDomain(), generator.generateInt());
}
private Activity copyActivity(Activity sourceActivity) {
Activity newActivity = new Activity(sourceActivity); // copy simple values : like name, category (but not Indicators, Attributes)
newActivity.getAttributeGroups().clear();
newActivity.getLockedPeriods().clear();
newActivity.getIndicators().clear();
// target db
newActivity.setDatabase(targetDb);
setLocationTypeForNewActivity(sourceActivity, newActivity);
em.persist(newActivity); // persist to get id of new activity
activityMapping.put(sourceActivity.getId(), newActivity);
return newActivity;
}
private void setLocationTypeForNewActivity(Activity sourceActivity, Activity newActivity) {
// location type -> change it only if sourceCountry != targetCountry
if (sourceActivity.getLocationType() != null && sourceDb.getCountry().getId() != targetDb.getCountry().getId()) {
boolean locationTypeCreated = false;
//1. If there is a location type with the same name in the new country, use that location Type
String sourceName = sourceActivity.getLocationType().getName();
if (!Strings.isNullOrEmpty(sourceName)) {
List<LocationType> locationTypes = em.createQuery("SELECT d FROM LocationType d WHERE Name = :activityName AND CountryId = :countryId")
.setParameter("activityName", sourceName)
.setParameter("countryId", targetDb.getCountry().getId())
.getResultList();
if (!locationTypes.isEmpty()) {
newActivity.setLocationType(locationTypes.get(0));
locationTypeCreated = true;
}
}
//2. if the source locationtype is bound to an adminlevel, choose the first root adminlevel in the new country
if (!locationTypeCreated && sourceActivity.getLocationType().getBoundAdminLevel() != null) {
List<LocationType> locationTypes = em.createQuery("SELECT d FROM LocationType d WHERE CountryId = :countryId")
.setParameter("countryId", targetDb.getCountry().getId())
.getResultList();
if (!locationTypes.isEmpty()) {
newActivity.setLocationType(locationTypes.get(0));
locationTypeCreated = true;
}
}
//3. Otherwise create new location type in the target country.
if (!locationTypeCreated) {
LocationType newLocationType = new LocationType();
newLocationType.setName(sourceActivity.getLocationType().getName());
newLocationType.setCountry(targetDb.getCountry());
newLocationType.setWorkflowId(sourceActivity.getLocationType().getWorkflowId());
newLocationType.setReuse(sourceActivity.getLocationType().isReuse());
em.persist(newLocationType);
newActivity.setLocationType(newLocationType);
}
}
}
private UserDatabase createDatabase(CloneDatabase command, User user) {
UserDatabase db = new UserDatabase();
db.setName(command.getName());
db.setFullName(command.getDescription());
db.setCountry(em.find(Country.class, command.getCountryId()));
db.setOwner(user);
em.persist(db);
return db;
}
}