| /******************************************************************************* |
| * Copyright (c) 2016 EclipseSource Services GmbH and others. |
| * 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: |
| * Martin Fleck - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.papyrus.compare.uml2.internal.hook.migration; |
| |
| import com.google.common.base.Function; |
| import com.google.common.base.Optional; |
| import com.google.common.collect.Maps; |
| |
| import java.util.Map; |
| import java.util.Map.Entry; |
| |
| import org.apache.commons.lang.StringUtils; |
| import org.eclipse.emf.common.util.URI; |
| import org.eclipse.emf.ecore.EObject; |
| import org.eclipse.emf.ecore.EPackage; |
| import org.eclipse.emf.ecore.resource.Resource; |
| import org.eclipse.emf.ecore.util.EcoreUtil; |
| import org.eclipse.papyrus.compare.uml2.internal.UMLPapyrusCompareMessages; |
| import org.eclipse.uml2.uml.Element; |
| import org.eclipse.uml2.uml.Profile; |
| import org.eclipse.uml2.uml.UMLPlugin; |
| import org.eclipse.uml2.uml.util.UMLUtil; |
| |
| /** |
| * This profile supplier returns the profile with the {@link EPackage#getNsURI()} package URI from the |
| * {@link UMLPlugin.getEPackageNsURIToProfileLocationMap() ProfileLocationMap}. The URI of all registered |
| * profile packages is compared to the given package URI and an appropriate one is chosen. If no acceptable |
| * match is found, this supplier may also return null. |
| * |
| * @author Martin Fleck <mfleck@eclipsesource.com> |
| */ |
| public class MissingProfileSupplier implements Function<EPackage, Profile> { |
| |
| /** |
| * The maximum distance between package URIs that is acceptable to justify an automated migration attempt. |
| */ |
| protected static final double DISTANCE_THRESHOLD = 0.25; |
| |
| /** |
| * Root element for which profiles are supplied. |
| */ |
| protected final Element root; |
| |
| /** |
| * Cache for profiles found based on EPackages. An Optional may hold a null reference if no profile has |
| * been found. |
| */ |
| private final Map<EPackage, Optional<Profile>> packageToProfileCache = Maps.newHashMap(); |
| |
| /** |
| * Creates a missing profile supplier that find a {@link Profile} for a given {@link EPackage}. Any |
| * successful or failed supply is marked in the elements {@link Resource.Diagnostic resource diagnostic}. |
| * |
| * @param root |
| * The element for which missing packages need to be found. |
| */ |
| public MissingProfileSupplier(final Element root) { |
| this.root = root; |
| } |
| |
| /** |
| * Returns the profile for the missing package, if such a profile can be found. This method uses a |
| * distance measure on the URI of the missing package and returns the closest, available package, under |
| * consideration of the {@link #DISTANCE_THRESHOLD}. |
| * |
| * @param missingPackage |
| * package for which a profile should be found |
| * @return an Optional with a profile, if possible |
| * @see #findProfile(EPackage) |
| */ |
| protected Optional<Profile> findClosestProfile(final EPackage missingPackage) { |
| final Map<String, Double> closestPackageUris = getClosestPackageURIs(missingPackage); |
| |
| Optional<Profile> foundProfile = Optional.absent(); |
| for (final Entry<String, Double> closestUri : closestPackageUris.entrySet()) { |
| // check if distance is small enough to justify automated migration |
| if (closestUri.getValue().doubleValue() <= DISTANCE_THRESHOLD) { |
| final String packageURI = closestUri.getKey(); |
| foundProfile = getProfileFromRegistry(packageURI); |
| if (foundProfile.isPresent()) { |
| return foundProfile; |
| } |
| } |
| } |
| return foundProfile; |
| } |
| |
| /** |
| * This method queries the {@link UMLPlugin#getEPackageNsURIToProfileLocationMap() profile location map} |
| * for known profile packages that have a {@link EPackage#getNsURI() URI} closest to the given package. |
| * The comparison of URIs is performed using the {@link #getDistance(String, String)} distance measure. |
| * |
| * @param missingPackage |
| * package for which similar packages need to be found |
| * @return a list of known profile packages with URIs closest to the given package. |
| */ |
| protected Map<String, Double> getClosestPackageURIs(final EPackage missingPackage) { |
| final Map<String, URI> profileLocationMap = UMLPlugin.getEPackageNsURIToProfileLocationMap(); |
| final Map<String, Double> closestPackageUris = Maps.newHashMap(); |
| double minDistance = Double.MAX_VALUE; |
| for (final String packageUri : profileLocationMap.keySet()) { |
| final double distance = getDistance(packageUri, missingPackage.getNsURI()); |
| if (distance == minDistance) { |
| closestPackageUris.put(packageUri, new Double(distance)); |
| } else if (distance < minDistance) { |
| minDistance = distance; |
| closestPackageUris.clear(); |
| closestPackageUris.put(packageUri, new Double(distance)); |
| } |
| } |
| return closestPackageUris; |
| } |
| |
| /** |
| * Returns the distance between the two given strings. A lower value indicates that the strings are more |
| * similar. |
| * |
| * @param left |
| * left string |
| * @param right |
| * right string |
| * @return distance measure, a lower value indicates that the strings are more similar |
| */ |
| protected double getDistance(final String left, final String right) { |
| double avgLength = Math.min(left.length(), right.length()) + (Math.abs(left.length() - right.length()) |
| / 2.0); |
| return StringUtils.getLevenshteinDistance(left, right) / avgLength; |
| } |
| |
| /** |
| * Returns the profile for the missing package, if such a profile can be found. The missing profile is |
| * determined through the Profile Namespace URI Pattern available in Papyrus starting with Eclipse Neon. |
| * With this pattern mechanism, profile providers can register their own namespace URI patterns to match |
| * the URI profile packages of different versions. |
| * |
| * @param missingPackage |
| * package for which a profile should be found |
| * @return an Optional with a profile, if possible |
| * @see #findClosestProfile(EPackage) |
| * @see https://bugs.eclipse.org/bugs/show_bug.cgi?id=496307 |
| */ |
| protected Optional<Profile> findMatchingProfile(final EPackage missingPackage) { |
| final String missingPackageURI = missingPackage.getNsURI(); |
| final Map<String, URI> profileLocationMap = UMLPlugin.getEPackageNsURIToProfileLocationMap(); |
| |
| Optional<Profile> foundProfile = Optional.absent(); |
| for (final String packageURI : profileLocationMap.keySet()) { |
| if (ProfileNamespaceURIPatternAPI.isEqualVersionlessNamespaceURI(missingPackageURI, packageURI)) { |
| foundProfile = getProfileFromRegistry(packageURI); |
| if (foundProfile.isPresent()) { |
| return foundProfile; |
| } |
| } |
| } |
| return foundProfile; |
| } |
| |
| /** |
| * Returns the profile with the given package URI by retrieving the package of the |
| * {@link EPackage.Registry} and calling {@link UMLUtil#getProfile(EPackage, |
| * org.eclipse.emf.ecore.EObject))} with the root element. Any retrieved results are cached. |
| * |
| * @param packageURI |
| * URI of the EPackage defining the profile |
| * @return The profile optional with the given packageURI which may or may not hold a null reference |
| */ |
| protected Optional<Profile> getProfileFromRegistry(final String packageURI) { |
| Optional<Profile> foundProfile = Optional.absent(); |
| final EPackage registeredPackage = EPackage.Registry.INSTANCE.getEPackage(packageURI); |
| if (registeredPackage != null) { |
| // check cache |
| foundProfile = packageToProfileCache.get(registeredPackage); |
| if (foundProfile == null) { |
| // no cache hit |
| Profile profile = UMLUtil.getProfile(registeredPackage, root); |
| foundProfile = Optional.fromNullable(profile); |
| // update cache |
| packageToProfileCache.put(registeredPackage, foundProfile); |
| } |
| } |
| return foundProfile; |
| } |
| |
| /** |
| * Returns a diagnostic that can be used to provide resource messages to the user. If this returns a |
| * {@link Throwable} diagnostic, any message is automatically converted to an ERROR by |
| * {@link EcoreUtil#computeDiagnostic(Resource, boolean)} when building the comparison scope. |
| * |
| * @param message |
| * message to be stored in the diagnostic |
| * @return diagnostic instance that can be attached to a resource |
| */ |
| protected Resource.Diagnostic createResourceDiagnostic(final String message) { |
| return new ProfileMigrationDiagnostic(message); |
| } |
| |
| /** |
| * Adds a warning to the roots {@link EObject#eResource() resource} using the diagnostic created by |
| * {@link #createResourceDiagnostic(String)}. |
| * |
| * @param message |
| * warning message |
| */ |
| protected void addResourceWarning(final String message) { |
| root.eResource().getWarnings().add(createResourceDiagnostic(message)); |
| } |
| |
| /** |
| * Adds a error to the roots {@link EObject#eResource() resource} using the diagnostic created by |
| * {@link #createResourceDiagnostic(String)}. |
| * |
| * @param message |
| * error message |
| */ |
| protected void addResourceError(final String message) { |
| root.eResource().getErrors().add(createResourceDiagnostic(message)); |
| } |
| |
| /** |
| * Returns the success message that is stored in the resource. |
| * |
| * @param missingPackage |
| * identification of the missing package |
| * @param profile |
| * identification of the profile that is used |
| * @return success message |
| */ |
| protected String getSuccessMessage(final String missingPackage, final String profile) { |
| return UMLPapyrusCompareMessages.getString("profile.migration.success", missingPackage, profile); //$NON-NLS-1$ |
| } |
| |
| /** |
| * Returns the fail message that is stored in the resource. |
| * |
| * @param missingPackage |
| * identification of the missing package |
| * @return fail message |
| */ |
| protected String getFailMessage(final String missingPackage) { |
| return UMLPapyrusCompareMessages.getString("profile.migration.fail", missingPackage); //$NON-NLS-1$ |
| } |
| |
| /** |
| * Returns the profile for the missing package, if such a profile can be found. The missing profile is |
| * determined through the Profile Namespace URI Pattern available in Papyrus starting with Eclipse Neon, |
| * if possible. If the necessary API is not available (previous Eclipse versions), we use a distance |
| * measure on the URI of the missing package and returns the closest, available package, under |
| * consideration of the {@link #DISTANCE_THRESHOLD}. |
| * |
| * @param missingPackage |
| * package for which a profile should be found |
| * @return an Optional with a profile, if possible |
| * @see #findMatchingProfile(EPackage) |
| * @see #findClosestProfile(EPackage) |
| */ |
| protected Optional<Profile> findProfile(final EPackage missingPackage) { |
| if (ProfileNamespaceURIPatternAPI.isAvailable()) { |
| return findMatchingProfile(missingPackage); |
| } |
| return findClosestProfile(missingPackage); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @return the profile that may host the missing package or null if no appropriate profile could be found |
| */ |
| public Profile apply(final EPackage missingPackage) { |
| // retrieve profile for missing package |
| Optional<Profile> foundProfile = findProfile(missingPackage); |
| if (foundProfile.isPresent()) { |
| // profile found, add information via resource warning |
| Profile profile = foundProfile.get(); |
| final String message = getSuccessMessage(missingPackage.getNsURI(), profile.getURI()); |
| addResourceWarning(message); |
| return profile; |
| } else { |
| // no matching profile found, add error to resource |
| final String message = getFailMessage(missingPackage.getNsURI()); |
| addResourceError(message); |
| return null; |
| } |
| } |
| } |