package org.sigmah.server.servlet.importer; /* * #%L * Sigmah * %% * Copyright (C) 2010 - 2016 URD * %% * 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.extjs.gxt.ui.client.data.BaseModelData; import com.extjs.gxt.ui.client.data.ModelData; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.sigmah.client.ui.presenter.CreateProjectPresenter; import org.sigmah.server.domain.User; import org.sigmah.shared.command.AmendmentActionCommand; import org.sigmah.shared.command.AutomatedImport; import org.sigmah.shared.command.CreateEntity; import org.sigmah.shared.command.UpdateProject; import org.sigmah.shared.command.result.CreateResult; import org.sigmah.shared.computation.value.ComputedValues; import org.sigmah.shared.dispatch.CommandException; import org.sigmah.shared.dispatch.FunctionalException; import org.sigmah.shared.dto.ElementExtractedValue; import org.sigmah.shared.dto.ImportDetails; import org.sigmah.shared.dto.ProjectDTO; import org.sigmah.shared.dto.base.EntityDTO; import org.sigmah.shared.dto.element.BudgetElementDTO; import org.sigmah.shared.dto.element.BudgetRatioElementDTO; import org.sigmah.shared.dto.element.BudgetSubFieldDTO; import org.sigmah.shared.dto.element.FlexibleElementDTO; import org.sigmah.shared.dto.element.event.ValueEvent; import org.sigmah.shared.dto.orgunit.OrgUnitDTO; import org.sigmah.shared.dto.referential.AmendmentAction; import org.sigmah.shared.dto.referential.AutomatedImportStatus; import org.sigmah.shared.dto.referential.BudgetSubFieldType; import org.sigmah.shared.dto.referential.ContainerInformation; import org.sigmah.shared.dto.referential.DefaultFlexibleElementType; import org.sigmah.shared.dto.referential.LogicalElementType; import org.sigmah.shared.dto.referential.LogicalElementTypes; import org.sigmah.shared.dto.referential.ProjectModelStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Wrap an importer to perform automatic and silent importation. * * @author Raphaƫl Calabro (raphael.calabro@netapsys.fr) */ public class AutomatedImporter { private static final Logger LOGGER = LoggerFactory.getLogger(AutomatedImporter.class); /** * Importer used to retrieve the data of the file. */ private final Importer importer; /** * Creates a new automated importer with the given <code>importer</code>. * <p> * The importer must be initialized. * * @param importer * Importer to use. * @see Importer#initialize() */ public AutomatedImporter(Importer importer) { this.importer = importer; } /** * Importation the data with the given configuration. * * @param configuration * Import configuration. * @return A list of status for */ public List<BaseModelData> importCorrespondances(AutomatedImport configuration) { final ArrayList<BaseModelData> result = new ArrayList<>(); while (importer.hasNext()) { final ImportDetails details = importer.next(); switch (details.getEntityStatus()) { case PROJECT_FOUND_CODE: case ORGUNIT_FOUND_CODE: result.add(onContainerFound(details, configuration)); break; case SEVERAL_PROJECTS_FOUND_CODE: case SEVERAL_ORGUNITS_FOUND_CODE: result.addAll(onSeveralContainersFound(details, configuration)); break; case PROJECT_LOCKED_CODE: result.add(onProjectLocked(details, configuration)); break; case PROJECT_NOT_FOUND_CODE: result.add(onProjectNotFound(details, configuration)); break; case ORGUNIT_NOT_FOUND_CODE: result.add(onOrgUnitNotFound(details, configuration)); break; default: throw new UnsupportedOperationException("Entity status '" + details.getEntityStatus() + "' is not supported."); } } return result; } /** * Update the project/orgunit found. * * @param details * Import details. * @param configuration * Import configuration. * @return A pair containing information about the container before the * update and the status of the importation. */ private BaseModelData onContainerFound(final ImportDetails details, final AutomatedImport configuration) { final Map.Entry<EntityDTO<Integer>, List<ElementExtractedValue>> singleEntry = details.getEntitiesToImport().entrySet().iterator().next(); return updateContainer(singleEntry.getKey(), singleEntry.getValue(), configuration); } /** * Update the project/orgunit found. * * @param container * Found container. * @param extractedValues * Values extracted from the imported file. * @param configuration * Import configuration. * @return A pair containing information about the container before the * update and the status of the importation. */ private BaseModelData updateContainer(final EntityDTO<Integer> container, final List<ElementExtractedValue> extractedValues, final AutomatedImport configuration) { AutomatedImportStatus status = AutomatedImportStatus.UPDATE_FAILED; try { updateContainerWithDetails(container, extractedValues, configuration.getFileName()); status = AutomatedImportStatus.UPDATED; } catch (FunctionalException ex) { LOGGER.error("A functional exception occured while importing values for the project #" + container.getId() + ".", ex); if (ex.getErrorCode() == FunctionalException.ErrorCode.ACCESS_DENIED) { status = AutomatedImportStatus.ACCESS_DENIED; } } catch (CommandException ex) { LOGGER.error("An error occured while importing values for the project #" + container.getId() + ".", ex); } return toBaseModelData(container, status); } /** * Update every project/orgunit if necessary. * * @param details * Import details. * @param configuration * Import configuration. * @return A list of pairs containing information about the containers * before the updates and the status of the importation. */ private List<BaseModelData> onSeveralContainersFound(final ImportDetails details, final AutomatedImport configuration) { final ArrayList<BaseModelData> result = new ArrayList<>(); if (configuration.isUpdateAllMatches()) { for (final Map.Entry<EntityDTO<Integer>, List<ElementExtractedValue>> entry : details.getEntitiesToImport().entrySet()) { result.add(updateContainer(entry.getKey(), entry.getValue(), configuration)); } } else { for (final Map.Entry<EntityDTO<Integer>, List<ElementExtractedValue>> entry : details.getEntitiesToImport().entrySet()) { result.add(toBaseModelData(entry.getKey(), AutomatedImportStatus.MULTIPLE_MATCHES_FOUND)); } } return result; } /** * Unlock the project if necessary and if the user has the required accesses * and update it. * * @param details * Import details. * @param configuration * Import configuration. * @return A pair containing information about the container before the * update and the status of the importation. */ private BaseModelData onProjectLocked(final ImportDetails details, final AutomatedImport configuration) { AutomatedImportStatus status; final Map.Entry<EntityDTO<Integer>, List<ElementExtractedValue>> singleEntry = details.getEntitiesToImport().entrySet().iterator().next(); final EntityDTO<Integer> container = singleEntry.getKey(); if (configuration.isUnlockProjectCores()) { try { importer.getExecutionContext().execute(new AmendmentActionCommand(singleEntry.getKey().getId(), AmendmentAction.UNLOCK)); updateContainerWithDetails(container, singleEntry.getValue(), configuration.getFileName()); status = AutomatedImportStatus.UNLOCKED_AND_UPDATED; } catch (CommandException e) { LOGGER.warn("An error occured while trying to unlock the project " + container, e); status = AutomatedImportStatus.UNLOCK_FAILED; } } else { status = AutomatedImportStatus.WAS_LOCKED; } return toBaseModelData(container, status); } /** * Create the project if necessary and if the user has the required accesses * and update it. * * @param details * Import details. * @param configuration * Import configuration. * @return A pair containing information about the container and the status * of the importation. */ private BaseModelData onProjectNotFound(final ImportDetails details, final AutomatedImport configuration) { AutomatedImportStatus status; final List<ElementExtractedValue> values = details.getEntitiesToImport().values().iterator().next(); final Map<String, Object> properties = toBasicProperties(values); ProjectDTO project = new ProjectDTO(); project.setName((String) properties.get(ProjectDTO.NAME)); project.setFullName((String) properties.get(ProjectDTO.FULL_NAME)); if (configuration.isCreateProjects()) { properties.putAll(toProjectCreationProperties(details)); try { project = createProjectWithProperties(properties); updateContainerWithDetails(project, values, configuration.getFileName()); status = AutomatedImportStatus.CREATED_AND_UPDATED; } catch (CommandException ex) { LOGGER.warn("An error occured while trying to create a new project.", ex); status = AutomatedImportStatus.CREATION_FAILED; } } else { status = AutomatedImportStatus.NOT_FOUND; } return toBaseModelData(project, status); } /** * Mark the given organizational unit as not found. * * @param details * Import details. * @param configuration * Import configuration. * @return A pair containing information about the container and the status * of the importation. */ private BaseModelData onOrgUnitNotFound(final ImportDetails details, final AutomatedImport configuration) { final List<ElementExtractedValue> values = details.getEntitiesToImport().values().iterator().next(); final Map<String, Object> properties = toBasicProperties(values); final OrgUnitDTO orgUnit = new OrgUnitDTO(); orgUnit.setName((String) properties.get(OrgUnitDTO.NAME)); orgUnit.setFullName((String) properties.get(OrgUnitDTO.FULL_NAME)); return toBaseModelData(orgUnit, AutomatedImportStatus.NOT_FOUND); } // -- // ENTITY CREATION AND UPDATE. // -- /** * Creates a new project with the given properties. * * @param projectProperties * Properties of the project to create. * @return The new project. * @throws CommandException * If an error occured during the creation of the project. */ private ProjectDTO createProjectWithProperties(final Map<String, Object> projectProperties) throws CommandException { final CreateResult createResult = importer.getExecutionContext().execute(new CreateEntity(ProjectDTO.ENTITY_NAME, projectProperties)); return (ProjectDTO) createResult.getEntity(); } /** * Update the given container with given elements. * * @param container * Container to update (can be a project or an org unit). * @param extractedValues * Extracted values to set. * @param fileName * Name of the imported file. */ private void updateContainerWithDetails(final EntityDTO<Integer> container, final List<ElementExtractedValue> extractedValues, final String fileName) throws CommandException { final ArrayList<ValueEvent> values = new ArrayList<>(); for (final ElementExtractedValue value : extractedValues) { final ValueEvent event = value != null ? value.toValueEvent() : null; if (event != null) { values.add(event); } } final UpdateProject updateProject = new UpdateProject(container.getId(), values, "Imported from file '" + fileName + "'."); importer.getExecutionContext().execute(updateProject); } // -- // UTILITY METHODS. // -- /** * Extract the basic properties of a project/orgunit (code, title and * budget) from the given values. * * @param values * Extracted values. * @return A map of the basic properties of a project/orgunit. */ private Map<String, Object> toBasicProperties(final List<ElementExtractedValue> values) { final HashMap<String, Object> projectProperties = new HashMap<String, Object>(); FlexibleElementDTO plannedBudgetElement = null; for (final ElementExtractedValue extractedValue : values) { final LogicalElementType type = LogicalElementTypes.of(extractedValue.getElement()); final DefaultFlexibleElementType defaultType = type.toDefaultFlexibleElementType(); if (defaultType != null) switch (defaultType) { case CODE: projectProperties.put(ProjectDTO.NAME, extractedValue.getNewValue()); break; case TITLE: projectProperties.put(ProjectDTO.FULL_NAME, extractedValue.getNewValue()); break; case BUDGET: final BudgetElementDTO budgetElement = (BudgetElementDTO) extractedValue.getElement(); for (BudgetSubFieldDTO budgetSubField : budgetElement.getBudgetSubFields()) { final Double value = (Double) extractedValue.getNewBudgetValues().get(budgetSubField.getId()); if (budgetSubField.getType() == BudgetSubFieldType.PLANNED && value != null) { projectProperties.put(ProjectDTO.BUDGET, value); } } break; case BUDGET_RATIO: final BudgetRatioElementDTO budgetRatioElement = (BudgetRatioElementDTO) extractedValue.getElement(); plannedBudgetElement = budgetRatioElement.getPlannedBudget(); break; default: break; } } if (plannedBudgetElement != null) { for (final ElementExtractedValue extractedValue : values) { if (plannedBudgetElement.equals(extractedValue.getElement())) { final String newValue = extractedValue.getNewValue() != null ? extractedValue.getNewValue().toString() : null; final Double value = ComputedValues.from(newValue).get(); if (value != null) { projectProperties.put(ProjectDTO.BUDGET, value); } break; } } } return projectProperties; } /** * Retrieve the properties required to create a new project. * <p> * The properties are:<ul> * <li>Creation mode (project or test project)</li> * <li>Project model identifier</li> * <li>Organizational unit identifier</li> * <li>Name of the calendar</li> * </ul> * * @param details * Importation details. * @return A map of the properties required to create a new project. */ private Map<String, Object> toProjectCreationProperties(final ImportDetails details) { final HashMap<String, Object> projectProperties = new HashMap<>(); final CreateProjectPresenter.Mode creationMode = details.getModelStatus() == ProjectModelStatus.DRAFT ? CreateProjectPresenter.Mode.TEST_PROJECT : CreateProjectPresenter.Mode.PROJECT; projectProperties.put(ProjectDTO.MODEL_ID, getProjectModelIdForName(details.getModelName())); projectProperties.put(ProjectDTO.ORG_UNIT_ID, getOrgUnitId()); projectProperties.put(ProjectDTO.CALENDAR_NAME, i18n("calendarDefaultName")); projectProperties.put(ProjectDTO.CREATION_MODE, creationMode); return projectProperties; } /** * Creates a new instance of <code>ContainerInformation</code> from the * given entity. * * @param container * Container to use (can be a project or an org unit). * @return A new instance of <code>ContainerInformation</code>. */ private ContainerInformation toContainerInformation(final EntityDTO<Integer> container) { if (container instanceof ProjectDTO) { final ProjectDTO project = (ProjectDTO) container; return new ContainerInformation(project.getId(), project.getName(), project.getFullName(), true); } else if (container instanceof OrgUnitDTO) { final OrgUnitDTO orgUnit = (OrgUnitDTO) container; return new ContainerInformation(orgUnit.getId(), orgUnit.getName(), orgUnit.getFullName(), false); } else { throw new IllegalArgumentException("Container should either be a project or an org unit."); } } private BaseModelData toBaseModelData(final EntityDTO<Integer> container, final AutomatedImportStatus status) { final BaseModelData modelData = new BaseModelData(); final ModelData source = (ModelData) container; modelData.set(ProjectDTO.ID, container.getId()); modelData.set(ProjectDTO.NAME, source.get(ProjectDTO.NAME)); modelData.set(ProjectDTO.FULL_NAME, source.get(ProjectDTO.FULL_NAME)); modelData.set("entityName", container.getEntityName()); modelData.set("status", status); return modelData; } /** * Find the identifier of one of the project model that has the given name. * If multiple models have the same name, result of this method is random. * * @param projectModelName * Name of the project model to search. * @return the identifier of a project model named with the given name. */ private Integer getProjectModelIdForName(final String projectModelName) { final List<Integer> projectModels = importer.em().createQuery("SELECT pm.id FROM ProjectModel AS pm WHERE pm.name = :name", Integer.class) .setParameter("name", projectModelName) .getResultList(); if (projectModels.isEmpty()) { throw new IllegalArgumentException("Project model '" + projectModelName + "' was not found."); } else if (projectModels.size() > 1) { LOGGER.warn("Multiple project models with name '" + projectModelName + "' exists, using #" + projectModels.get(0) + " for the creation."); } return projectModels.get(0); } /** * Find the identifier of the organizational unit of the current user. * * @return the identifier of the orgunit of the current user. */ private String getOrgUnitId() { final User user = importer.getExecutionContext().getUser(); final Integer orgUnitId = user.getMainOrgUnitWithProfiles().getOrgUnit().getId(); if (orgUnitId != null) { return orgUnitId.toString(); } else { throw new IllegalArgumentException("Current user has no organizational unit."); } } /** * Find the translate value for the given <code>key</code> using the * language of the current user. * * @param key * Key of the message to translate. * @return The translated key. */ private String i18n(String key) { return importer.getTranslator().t(importer.getExecutionContext().getLanguage(), key); } }