/***************************************************************************** * Copyright (c) 2011 Atos Origin. * * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Atos Origin - Initial API and implementation * *****************************************************************************/ package org.eclipse.papyrus.uml.controlmode.profile.validation; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.emf.common.notify.Notification; import org.eclipse.emf.common.util.TreeIterator; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.InternalEObject; import org.eclipse.emf.ecore.impl.ENotificationImpl; import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain; import org.eclipse.emf.edit.domain.EditingDomain; import org.eclipse.emf.validation.AbstractModelConstraint; import org.eclipse.emf.validation.EMFEventType; import org.eclipse.emf.validation.IValidationContext; import org.eclipse.gmf.runtime.emf.core.util.EMFCoreUtil; import org.eclipse.osgi.util.NLS; import org.eclipse.papyrus.infra.core.utils.EditorUtils; import org.eclipse.papyrus.infra.services.resourceloading.preferences.StrategyChooser; import org.eclipse.papyrus.infra.widgets.toolbox.notification.builders.NotificationBuilder; import org.eclipse.papyrus.uml.controlmode.profile.Activator; import org.eclipse.papyrus.uml.controlmode.profile.Messages; import org.eclipse.papyrus.uml.controlmode.profile.helpers.ProfileApplicationHelper; import org.eclipse.uml2.uml.Package; import org.eclipse.uml2.uml.Profile; import org.eclipse.uml2.uml.ProfileApplication; import org.eclipse.uml2.uml.UMLPackage; /** * Check if profile application is correctly duplicated on all controlled sub-packages and duplicate it if needed. * In case of deletion, it also removes unnecessary profile applications. * * @author vhemery */ public class ProfileApplicationDuplicationChecker extends AbstractModelConstraint { /** * This is a result which is intended to be set with a boolean value with notification runables. * * @author vhemery */ public class BooleanResult { boolean value = false; /** * Get result * * @return boolean result value */ public boolean getValue() { return value; } /** * Set result * * @param pValue * boolean result value */ public void setValue(boolean pValue) { value = pValue; } } /** Format String for a list entry */ private static final String ENTRY_FORMAT = "<li>%s</li>"; /** Constant for load all loading strategy */ protected static final int LOAD_ALL_STRATEGY = 0; private IValidationContext lastValidatedContext = null; /** * Check if profile applications are correctly duplicated on controlled package and that there is no useless copy left. * * @see org.eclipse.emf.validation.AbstractModelConstraint#validate(org.eclipse.emf.validation.IValidationContext) * * @param ctx * validation context * @return validation status */ public IStatus validate(IValidationContext ctx) { try { if(ctx.equals(lastValidatedContext)) { return ctx.createSuccessStatus(); } else { lastValidatedContext = ctx; } EObject eObject = ctx.getTarget(); // detect profile application creation if((EMFEventType.ADD.equals(ctx.getEventType()) || EMFEventType.ADD_MANY.equals(ctx.getEventType())) && ctx.getFeatureNewValue() instanceof ProfileApplication) { ProfileApplication profileAppl = (ProfileApplication)ctx.getFeatureNewValue(); Package pack = (Package)eObject; boolean res = stereotypeApplicationAdded(pack, profileAppl); if(!res) { return ctx.createFailureStatus(); } } // detect profile application deletion else if(((EMFEventType.REMOVE.equals(ctx.getEventType()) && ctx.getFeatureNewValue() instanceof ProfileApplication) || EMFEventType.REMOVE_MANY.equals(ctx.getEventType()))) { Map<ProfileApplication, Profile> oldAssignement = new HashMap<ProfileApplication, Profile>(ctx.getAllEvents().size()); for(Notification n : ctx.getAllEvents()) { // case when profile is removed from profile application : keep the reference if(ENotificationImpl.SET == n.getEventType() && UMLPackage.eINSTANCE.getProfileApplication_AppliedProfile().equals(n.getFeature()) && n.getNotifier() instanceof ProfileApplication) { Profile profile = (Profile)n.getOldValue(); ProfileApplication profileAppl = (ProfileApplication)n.getNotifier(); oldAssignement.put(profileAppl, profile); } // cases when profile application is removed if(ENotificationImpl.REMOVE == n.getEventType() && n.getOldValue() instanceof ProfileApplication && n.getNotifier() instanceof Package) { ProfileApplication profileAppl = (ProfileApplication)n.getOldValue(); Package pack = (Package)n.getNotifier(); Profile profile = profileAppl.getAppliedProfile(); if(profile == null) { profile = oldAssignement.get(profileAppl); } if(profile != null) { boolean res = stereotypeApplicationRemoved(pack, profileAppl, profile); if(!res) { return ctx.createFailureStatus(); } } } } } return ctx.createSuccessStatus(); } catch (RuntimeException rte) { // avoid throwing uncaught exception which would disable the constraint Activator.getDefault().getLog().log(new Status(Status.ERROR, Activator.PLUGIN_ID, Messages.error_during_validation, rte)); // ensure that the constraint's failure does not prevent modification return ctx.createSuccessStatus(); } } /** * Handle the case when a stereotype application is added : * - Inspect controlled sub-packages * - Duplicate profile applicationss on these * - Create eAnnotation for duplicated profiles * * @param packageElement * the package on which stereotype application has been added * @param profileAppl * the added profile application * @return whether addition is allowed */ private boolean stereotypeApplicationAdded(Package packageElement, ProfileApplication profileAppl) { //Inspect controlled sub-packages Set<Package> controlledPack = getControlledSubPackages(packageElement); boolean update = checkControlledPackagesUpdateable(controlledPack); if(update) { for(Package pack : controlledPack) { ProfileApplicationHelper.duplicateProfileApplication(pack, profileAppl.getAppliedProfile()); } return true; } else { return false; } } /** * Handle the case when a stereotype application is removed : * - Forbid direct removal of a duplicated profile application (with eAnnotation) * - Inspect controlled sub-packages * - Remove duplicated profile applications on these (with eAnnotation) * * @param packageElement * the package from which stereotype application has been removed * @param profileAppl * the removed profile application * @param profile * the unapplied profile * @return whether removal is allowed */ private boolean stereotypeApplicationRemoved(Package packageElement, ProfileApplication profileAppl, Profile profile) { //Forbid direct removal of a duplicated profile application (with eAnnotation) if(ProfileApplicationHelper.isDuplicatedProfileApplication(profileAppl)) { Package parentPack = ProfileApplicationHelper.getParentPackageWithProfile(packageElement, profile, true); // restore stereotype application when it is called from parent intermediate package ProfileApplicationHelper.duplicateProfileApplication(packageElement, profile); String msg; if(parentPack != null) { msg = NLS.bind(Messages.warning_cannot_delete_duplicated, EMFCoreUtil.getQualifiedName(packageElement, true), EMFCoreUtil.getQualifiedName(parentPack, true)); } else { // parent package can not be reached as it is in a different maybe not accessible resource (working on controlled resource) msg = NLS.bind(Messages.warning_cannot_delete_duplicated_alt, EMFCoreUtil.getQualifiedName(packageElement, true)); } NotificationBuilder notifBuild = NotificationBuilder.createAsyncPopup(msg); notifBuild.run(); return true; } //Inspect controlled sub-packages Set<Package> controlledPack = getControlledSubPackages(packageElement); boolean update = checkControlledPackagesUpdateable(controlledPack); if(update) { for(Package pack : controlledPack) { ProfileApplicationHelper.removeProfileApplicationDuplication(pack, profile, false); } return true; } else { return false; } } /** * Check if controlled sub-packages can be correctly updated : * - Check if controlled package is loaded * - Change the control strategy if necessary * - Report error if the controlled package is read-only * * @param controlledPackages * the controlled sub-packages (may be updated if contains proxies) * @return true if can be updated */ private boolean checkControlledPackagesUpdateable(Set<Package> controlledPackages) { boolean notLoadedPackages = false; StringBuffer notLoadedPackagesList = new StringBuffer(); boolean readOnlyPackages = false; StringBuffer readOnlyPackagesList = new StringBuffer(); //Check if controlled package is loaded for(Iterator<Package> iterator = controlledPackages.iterator(); iterator.hasNext();) { Package pack = iterator.next(); EditingDomain domain = EditorUtils.getTransactionalEditingDomain(); if(pack.eIsProxy()) { EObject loadedObject = domain.getResourceSet().getEObject(((InternalEObject)pack).eProxyURI(), true); if(loadedObject != null) { // pack has been reload, replace proxy; controlledPackages.remove(pack); pack = (Package)loadedObject; controlledPackages.add(pack); } } if(pack.eIsProxy()) { notLoadedPackages = true; URI uri = ((InternalEObject)pack).eProxyURI(); String uriLastSeg = uri.lastSegment(); String name = uriLastSeg.substring(0, uriLastSeg.length() - uri.fileExtension().length() - 1); String qualifName = EMFCoreUtil.getQualifiedName(pack.getOwner(), true).concat("::").concat(name);//$NON-NLS-1$ notLoadedPackagesList.append(String.format(ENTRY_FORMAT, qualifName)); } else { if(domain instanceof AdapterFactoryEditingDomain) { // reset read-only cache map ((AdapterFactoryEditingDomain)domain).getResourceToReadOnlyMap().clear(); } if(domain.isReadOnly(pack.eResource())) { readOnlyPackages = true; String name = EMFCoreUtil.getQualifiedName(pack, true); readOnlyPackagesList.append(String.format(ENTRY_FORMAT, name)); } } } //Report error if the controlled package is read-only if(readOnlyPackages) { String msg = NLS.bind(Messages.error_readonly, readOnlyPackagesList.toString()); NotificationBuilder notifBuild = NotificationBuilder.createErrorPopup(msg); notifBuild.setHTML(true); notifBuild.run(); return false; } //Change the control strategy if necessary if(notLoadedPackages) { String msg = NLS.bind(Messages.switch_loading_strategy, notLoadedPackagesList.toString()); final BooleanResult stategyChanged = new BooleanResult(); Runnable runStrategySwitch = new Runnable() { public void run() { StrategyChooser.setCurrentStrategy(LOAD_ALL_STRATEGY); stategyChanged.setValue(true); } }; Runnable cancel = new Runnable() { public void run() { stategyChanged.setValue(false); } }; NotificationBuilder notifBuild = NotificationBuilder.createYesNo(msg, runStrategySwitch, cancel); notifBuild.setHTML(true); notifBuild.setAsynchronous(false); notifBuild.run(); if(stategyChanged.getValue()) { // refresh set controlledPackages return checkControlledPackagesUpdateable(controlledPackages); } else { return false; } } return true; } /** * Get the controlled children packages * * @param packageElement * package to inspect children * @return set of children packages which are controlled */ private Set<Package> getControlledSubPackages(Package packageElement) { Set<Package> controlledPackages = new HashSet<Package>(); TreeIterator<EObject> iterator = packageElement.eAllContents(); while(iterator.hasNext()) { EObject child = iterator.next(); if(child instanceof Package) { // despite what AdapterFactoryEditingDomain#isControlled says, a not loaded child is controlled if(AdapterFactoryEditingDomain.isControlled(child) || child.eIsProxy()) { controlledPackages.add((Package)child); } } else { iterator.prune(); } } return controlledPackages; } }