package org.sigmah.server.handler; /* * #%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 java.util.Date; import java.util.List; import org.sigmah.server.dispatch.impl.UserDispatch.UserExecutionContext; import org.sigmah.server.domain.Project; import org.sigmah.server.domain.User; import org.sigmah.server.domain.element.FlexibleElement; import org.sigmah.server.domain.value.Value; import org.sigmah.server.handler.base.AbstractCommandHandler; import org.sigmah.server.mapper.Mapper; import org.sigmah.shared.command.UpdateProject; import org.sigmah.shared.command.result.VoidResult; import org.sigmah.shared.dispatch.CommandException; import org.sigmah.shared.dto.element.FlexibleElementDTO; import org.sigmah.shared.dto.element.event.ValueEventWrapper; import org.sigmah.shared.dto.referential.DefaultFlexibleElementType; import org.sigmah.shared.dto.referential.ProjectModelStatus; import org.sigmah.shared.dto.value.TripletValueDTO; import org.sigmah.shared.util.ValueResultUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Inject; import com.google.inject.persist.Transactional; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Set; import org.sigmah.offline.sync.SuccessCallback; import org.sigmah.server.computation.ServerComputations; import org.sigmah.server.computation.ServerValueResolver; import org.sigmah.server.domain.OrgUnit; import org.sigmah.server.domain.element.DefaultFlexibleElement; import org.sigmah.server.handler.util.Conflicts; import org.sigmah.server.handler.util.Handlers; import org.sigmah.server.i18n.I18nServer; import org.sigmah.server.service.ValueService; import org.sigmah.shared.Language; import org.sigmah.shared.command.result.ValueResult; import org.sigmah.shared.computation.Computation; import org.sigmah.shared.computation.Computations; import org.sigmah.shared.computation.dependency.CollectionDependency; import org.sigmah.shared.computation.dependency.ContributionDependency; import org.sigmah.shared.computation.dependency.Dependency; import org.sigmah.shared.computation.dependency.DependencyVisitor; import org.sigmah.shared.computation.dependency.SingleDependency; import org.sigmah.shared.computation.value.ComputedValue; import org.sigmah.shared.computation.value.ComputedValues; import org.sigmah.shared.dispatch.FunctionalException; import org.sigmah.shared.dispatch.UpdateConflictException; import org.sigmah.shared.dto.element.BudgetElementDTO; import org.sigmah.shared.dto.element.BudgetSubFieldDTO; import org.sigmah.shared.dto.element.ComputationElementDTO; import org.sigmah.shared.dto.profile.ProfileDTO; import org.sigmah.shared.dto.referential.AmendmentState; import org.sigmah.shared.dto.referential.ContainerInformation; import org.sigmah.shared.dto.referential.GlobalPermissionEnum; import org.sigmah.shared.dto.referential.LogicalElementType; import org.sigmah.shared.dto.referential.LogicalElementTypes; import org.sigmah.shared.dto.value.ListableValue; import org.sigmah.shared.util.ProfileUtils; /** * Updates the values of the flexible elements for a specific project. * * @author Raphaƫl Calabro (rcalabro@ideia.fr) */ public class UpdateProjectHandler extends AbstractCommandHandler<UpdateProject, VoidResult> { /** * Logger. */ private final static Logger LOGGER = LoggerFactory.getLogger(UpdateProjectHandler.class); /** * Service handling the update of Value objects. */ @Inject private ValueService valueService; /** * Mapper to transform domain objects in DTO. */ @Inject private Mapper mapper; /** * Language files. */ @Inject private I18nServer i18nServer; /** * Conflict detector. */ @Inject private Conflicts conflictHandler; /** * Value resolver for computations. */ @Inject private ServerValueResolver valueResolver; /** * {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public VoidResult execute(final UpdateProject cmd, final UserExecutionContext context) throws CommandException { if (LOGGER.isDebugEnabled()) { LOGGER.debug("[execute] Updates project #" + cmd.getProjectId() + " with following values #" + cmd.getValues().size() + " : " + cmd.getValues()); } final List<ValueEventWrapper> values = cmd.getValues(); final Integer projectId = cmd.getProjectId(); final String comment = cmd.getComment(); updateProject(values, projectId, context, comment); return null; } /** * Update the project identified by <code>projectId</code> with the given values. * * @param values Values to update. * @param projectId Identifier of the project to update. * @param context User context. * @param comment Update comment. * @throws CommandException If an error occurs during update. */ @Transactional(rollbackOn = CommandException.class) protected void updateProject(final List<ValueEventWrapper> values, final Integer projectId, UserExecutionContext context, String comment) throws CommandException { // This date must be the same for all the saved values ! final Date historyDate = new Date(); final User user = context.getUser(); final ContainerInformation containerInformation; // Search the given project. final Project project = em().find(Project.class, projectId); containerInformation = throwIfProjectOrOrgUnitIsNotEditable(project, projectId, user, context.getLanguage()); // Verify if the modifications conflicts with the project state. final List<String> conflicts = searchForConflicts(project, values, context); // Track if an element part of the core version has been modified. boolean coreVersionHasBeenModified = false; // Iterating over the value change events for (final ValueEventWrapper valueEvent : values) { // Event parameters. final FlexibleElementDTO source = valueEvent.getSourceElement(); final FlexibleElement element = em().find(FlexibleElement.class, source.getId()); final TripletValueDTO updateListValue = valueEvent.getTripletValue(); final String updateSingleValue = valueEvent.getSingleValue(); final LogicalElementType type = LogicalElementTypes.of(source); final Set<Integer> multivaluedIdsValue = valueEvent.getMultivaluedIdsValue(); final Integer iterationId = valueEvent.getIterationId(); LOGGER.debug("[execute] Updates value of element #{} ({})", source.getId(), source.getEntityName()); LOGGER.debug("[execute] Event of type {} with value {} and list value {} (iteration : {}).", valueEvent.getChangeType(), updateSingleValue, updateListValue, iterationId); // Verify if the core version has been modified. coreVersionHasBeenModified = coreVersionHasBeenModified || element != null && element.isAmendable(); if (type.toDefaultFlexibleElementType() != null && type.toDefaultFlexibleElementType() != DefaultFlexibleElementType.BUDGET) { // Case of the default flexible element which values arent't stored // like other values. These values impact directly the project. valueService.saveValue(updateSingleValue, valueEvent.isProjectCountryChanged(), historyDate, (DefaultFlexibleElement) element, projectId, user, comment); } else if (updateSingleValue != null) { valueService.saveValue(updateSingleValue, historyDate, element, projectId, iterationId, user, comment); } else if (multivaluedIdsValue != null) { valueService.saveValue(multivaluedIdsValue, valueEvent, historyDate, element, projectId, iterationId, user, comment); } else if (updateListValue != null) { // Special case : this value is a part of a list which is the true value of the flexible element. (only used for // the TripletValue class for the moment) valueService.saveValue(updateListValue, valueEvent.getChangeType(), historyDate, element, projectId, iterationId, user, comment); } else { LOGGER.warn("Empty value event received for element #{} ({}) of container #{}.", source.getId(), source.getEntityName(), projectId); } } final Project updatedProject = em().find(Project.class, projectId); if (updatedProject == null) { if(coreVersionHasBeenModified) { // Update the revision number updatedProject.setAmendmentRevision(updatedProject.getAmendmentRevision() == null ? 2 : updatedProject.getAmendmentRevision() + 1); em().merge(updatedProject); } } if (!conflicts.isEmpty()) { // A conflict was found. throw new UpdateConflictException(containerInformation, conflicts.toArray(new String[0])); } } /** * Ensure that the user has edition access for the updated container. * * @param project * Updated project (or <code>null</code> if an org unit is being updated). * @param containerId * Identifier of the updated container. * @param user * User trying to update the container. * @param language * Language used by the user. * @return The informations about the updated container. * @throws IllegalStateException If the user can not edit the given container. */ private ContainerInformation throwIfProjectOrOrgUnitIsNotEditable(final Project project, final Integer containerId, final User user, final Language language) throws CommandException { if (project != null) { if (!Handlers.isProjectEditable(project, user)) { throw new FunctionalException(FunctionalException.ErrorCode.ACCESS_DENIED, i18nServer.t(language, "project"), project.getId().toString(), user.getId().toString()); } return project.toContainerInformation(); } else { // If project is null, it means the user is not trying to update a project but an org unit final OrgUnit orgUnit = em().find(OrgUnit.class, containerId); if (!Handlers.isOrgUnitVisible(orgUnit, user)) { throw new FunctionalException(FunctionalException.ErrorCode.ACCESS_DENIED, i18nServer.t(language, "orgunit"), orgUnit.getId().toString(), user.getId().toString()); } return orgUnit.toContainerInformation(); } } /** * Format the given value event. * * @param valueEvent * Value event to format. * @return The given value in HTML format. */ public String getTargetValueFormatted(ValueEventWrapper valueEvent) { return valueEvent.getSourceElement().toHTML(valueEvent.getSingleValue()); } /** * Throw a functional exception if a conflict if found. * * @param project Updated project. * @param values Values to update. * @param projectId * @throws FunctionalException */ private List<String> searchForConflicts(final Project project, final List<ValueEventWrapper> values, final UserExecutionContext context) throws FunctionalException { final ArrayList<String> conflicts = new ArrayList<>(); if (project == null) { // The user is modifying an org unit. // TODO: Verify if the user has the right to modify the org unit. return conflicts; } Integer projectOrgUnitId = null; if (project.getOrgUnit() == null) { // The project should be a draft project // Let's verify it if (project.getProjectModel().getStatus() != ProjectModelStatus.DRAFT) { LOGGER.error("Project {} doesn't have an OrgUnit.", project.getId()); } else if (context.getUser().getMainOrgUnitWithProfiles() == null) { LOGGER.error("User {} doesn't have a main org unit.", context.getUser().getId()); } else { // Let's get the main org unit from the user projectOrgUnitId = context.getUser().getMainOrgUnitWithProfiles().getOrgUnit().getId(); } } else { projectOrgUnitId = project.getOrgUnit().getId(); } final Language language = context.getLanguage(); ProfileDTO profile = null; if (projectOrgUnitId != null) { profile = Handlers.aggregateProfiles(context.getUser(), mapper).get(projectOrgUnitId); } if (project.getProjectModel().isUnderMaintenance()) { // BUGFIX #730: Verifying the maintenance status of projects. conflicts.add(i18nServer.t(language, "conflictEditingUnderMaintenanceProject", project.getName(), project.getFullName())); return conflicts; } // Verify computated values. conflictsRelatedToComputedElements(values, project, conflicts, language); if (ProfileUtils.isGranted(profile, GlobalPermissionEnum.MODIFY_LOCKED_CONTENT)) { // The user is allowed to edit locked fields. final boolean projectIsClosed = project.getCloseDate() != null; final boolean projectIsLocked = project.getAmendmentState() == AmendmentState.LOCKED; for (final ValueEventWrapper value : values) { final FlexibleElementDTO source = value.getSourceElement(); final boolean phaseIsClosed = conflictHandler.isParentPhaseClosed(source.getId(), project.getId()); if (projectIsClosed || phaseIsClosed || (source.getAmendable() && projectIsLocked)) { final ValueResult result = new ValueResult(); result.setValueObject(value.getSingleValue()); result.setValuesObject(value.getTripletValue() != null ? Collections.<ListableValue>singletonList(value.getTripletValue()) : null); if(!source.isCorrectRequiredValue(result)) { conflicts.add(i18nServer.t(language, "conflictModifyLockedContentEmptyValue", source.getFormattedLabel(), valueService.getCurrentValueFormatted(project.getId(), source))); } } } return conflicts; } if (project.getCloseDate() != null) { // User is trying to modify a closed project. for (final ValueEventWrapper valueEvent : values) { final FlexibleElementDTO source = valueEvent.getSourceElement(); conflicts.add(i18nServer.t(language, "conflictUpdatingAClosedProject", source.getFormattedLabel(), valueService.getCurrentValueFormatted(project.getId(), source), getTargetValueFormatted(valueEvent))); } } else { // Verify if the user is trying to modify a closed phase. Iterator<ValueEventWrapper> iterator = values.iterator(); while (iterator.hasNext()) { final ValueEventWrapper valueEvent = iterator.next(); final FlexibleElementDTO source = valueEvent.getSourceElement(); if (conflictHandler.isParentPhaseClosed(source.getId(), project.getId())) { // Removing the current value event from the update list. iterator.remove(); conflicts.add(i18nServer.t(language, "conflictUpdatingAClosedPhase", source.getFormattedLabel(), valueService.getCurrentValueFormatted(project.getId(), source), getTargetValueFormatted(valueEvent))); } } // Verify if the user is trying to modify a locked field. if (project.getAmendmentState() == AmendmentState.LOCKED) { iterator = values.iterator(); while (iterator.hasNext()) { final ValueEventWrapper valueEvent = iterator.next(); final FlexibleElementDTO source = valueEvent.getSourceElement(); final boolean conflict; if (source.getAmendable()) { if (source instanceof BudgetElementDTO) { final BudgetSubFieldDTO divisorField = ((BudgetElementDTO)source).getRatioDivisor(); final Value value = valueService.retrieveCurrentValue(project.getId(), source.getId(), valueEvent.getIterationId()); conflict = getValueOfSubField(value.getValue(), divisorField) != getValueOfSubField(valueEvent.getSingleValue(), divisorField); } else { conflict = true; } } else { conflict = false; } if (conflict) { // Removing the current value event from the update list. iterator.remove(); conflicts.add(i18nServer.t(language, "conflictUpdatingALockedField", source.getFormattedLabel(), valueService.getCurrentValueFormatted(project.getId(), source), getTargetValueFormatted(valueEvent))); } } } } return conflicts; } /** * Verify updates done to computed values. * * @param values * Changed values. * @param project * Edited project. * @param conflicts * List of conflicts. * @param language * Language of the user. */ private void conflictsRelatedToComputedElements(final List<ValueEventWrapper> values, final Project project, final List<String> conflicts, final Language language) { for (final ValueEventWrapper value : values) { final FlexibleElementDTO source = value.getSourceElement(); if (source instanceof ComputationElementDTO && ((ComputationElementDTO) source).hasConstraints()) { // Recompute the value and check that the result matches the constraints. final ComputationElementDTO computationElement = (ComputationElementDTO) source; final ComputedValue[] serverResult = new ComputedValue[1]; final ComputedValue clientResult = ComputedValues.from(value.getSingleValue()); final Computation computation = Computations.parse(computationElement.getRule(), ServerComputations.getAllElementsFromModel(project.getProjectModel())); computation.computeValueWithWrappersAndResolver(project.getId(), values, valueResolver, new SuccessCallback<String>() { @Override public void onSuccess(String result) { serverResult[0] = ComputedValues.from(result); } }); if (!clientResult.equals(serverResult[0])) { // Updating the value. value.setSingleValue(serverResult[0].toString()); } final int comparison = serverResult[0].matchesConstraints(computationElement); if (comparison != 0) { final String greaterOrLess, breachedConstraint; if (comparison < 0) { greaterOrLess = i18nServer.t(language, "flexibleElementComputationLess"); breachedConstraint = computationElement.getMinimumValue(); } else { greaterOrLess = i18nServer.t(language, "flexibleElementComputationGreater"); breachedConstraint = computationElement.getMaximumValue(); } final List<ValueEventWrapper> changes = computation.getRelatedChanges(values); final String fieldList = org.sigmah.shared.util.Collections.join(changes, new org.sigmah.shared.util.Collections.Mapper<ValueEventWrapper, String>() { @Override public String forEntry(ValueEventWrapper entry) { return entry.getSourceElement().getFormattedLabel(); } }, ", "); conflicts.add(i18nServer.t(language, "conflictComputationOutOfBound", fieldList, value.getSingleValue(), source.getFormattedLabel(), greaterOrLess, breachedConstraint) + dependenciesLastValuesForComputation(computation, project.getId(), value.getIterationId(), language)); } } } } /** * Returns a list of the details of each dependency of the computation. <br> * <br> * The details for each dependency contains:<ul> * <li>Label of the flexible element.</li> * <li>Last saved value (or '-' if unmodified).</li> * <li>Short name of the author of the last modification (or '-' if unmodified).</li> * <li>Date of the last modification (or '-' if unmodified).</li> * </ul> * * @param computation * Computation. * @param projectId * Identifier of the current project. * @param language * Language to use to create the messages. * @return A list of details about the dependencies. * * @see #flexibleElementDetails(org.sigmah.shared.dto.element.FlexibleElementDTO, int, Integer, org.sigmah.shared.Language, java.text.DateFormat) */ private String dependenciesLastValuesForComputation(final Computation computation, final int projectId, final Integer iterationId, final Language language) { final DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.forLanguageTag(language.getLocale())); return org.sigmah.shared.util.Collections.join(computation.getDependencies(), new org.sigmah.shared.util.Collections.Mapper<Dependency, String>() { @Override public String forEntry(final Dependency entry) { final StringBuilder stringBuilder = new StringBuilder(); entry.accept(new DependencyVisitor() { @Override public void visit(SingleDependency dependency) { stringBuilder .append("\n") .append(flexibleElementDetails(dependency.getFlexibleElement(), projectId, iterationId, language, formatter)); } @Override public void visit(CollectionDependency dependency) { throw new UnsupportedOperationException("Not supported yet."); } @Override public void visit(ContributionDependency dependency) { throw new UnsupportedOperationException("Not supported yet."); } }); return stringBuilder.toString(); } }, ""); } /** * Finds the current value of the given element and returns a line with the details. * <br> * <br> * The returned details are:<ul> * <li>Label of the flexible element.</li> * <li>Last saved value (or '-' if unmodified).</li> * <li>Short name of the author of the last modification (or '-' if unmodified).</li> * <li>Date of the last modification (or '-' if unmodified).</li> * </ul> * * @param entry * Flexible element to format. * @param projectId * Identifier of the project. * @param language * Language to use to creates the message. * @param formatter * Date formatter. * @return The formatted line. */ private String flexibleElementDetails(final FlexibleElementDTO entry, final int projectId, Integer iterationId, final Language language, final DateFormat formatter) { final String title = entry.getFormattedLabel(); final String value, author, date; final Value currentValue = valueService.retrieveCurrentValue(projectId, entry.getId(), iterationId); if (currentValue != null) { value = currentValue.getValue(); author = User.getUserShortName(currentValue.getLastModificationUser()); date = formatter.format(currentValue.getLastModificationDate()); } else { value = "-"; author = "-"; date = "-"; } return i18nServer.t(language, "conflictComputationDependencyDetails", title, value, author, date); } /** * Retrieves the value of the given field as a double. * * @param valueResult * Raw value of a budget element. * @param budgetSubField * Sub field to search. * @return The value of the given budget sub field. */ private double getValueOfSubField(final String valueResult, final BudgetSubFieldDTO budgetSubField) { final Map<Integer, String> values = ValueResultUtils.splitMapElements(valueResult); final String value = values.get(budgetSubField.getId()); if (value != null && value.matches("^[0-9]+(([.][0-9]+)|)$")) { return Double.parseDouble(value); } else { return 0.0; } } }