XmiReferenceResolverImpl.java
/* $Id$
*****************************************************************************
* Copyright (c) 2005-2012 Contributors - see below
* 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:
* Tom Morris
* euluis
*****************************************************************************
*
* Some portions of this file was previously release using the BSD License:
*/
// Copyright (c) 2005-2008 The Regents of the University of California. All
// Rights Reserved. Permission to use, copy, modify, and distribute this
// software and its documentation without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph appear in all copies. This software program and
// documentation are copyrighted by The Regents of the University of
// California. The software program and documentation are supplied "AS
// IS", without any accompanying services from The Regents. The Regents
// does not warrant that the operation of the program will be
// uninterrupted or error-free. The end-user understands that the program
// was developed for research purposes and is advised not to rely
// exclusively on the program for any reason. IN NO EVENT SHALL THE
// UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
// SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT,
// UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
package org.argouml.model.mdr;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.jmi.reflect.RefObject;
import javax.jmi.reflect.RefPackage;
import javax.net.ssl.HttpsURLConnection;
import org.argouml.model.UmlException;
import org.argouml.model.XmiReferenceRuntimeException;
import org.netbeans.api.xmi.XMIInputConfig;
import org.netbeans.lib.jmi.util.DebugException;
import org.netbeans.lib.jmi.xmi.XmiContext;
import org.xml.sax.InputSource;
/**
* Custom resolver to use with XMI reader.
* <p>
*
* This provides two functions:
* <nl>
* <li>Records the mapping of <code>xmi.id</code>'s to MDR objects as they
* are resolved so that the map can be used to lookup objects by xmi.id later
* (used by diagram subsystem to associate GEF/PGML objects with model
* elements). This map is also used to resolve cross references (HREFs) to
* other files when reading multiple files linked together.
* <li>Keeps an inverse map of objects to the xmi.id that they were read in from
* which can be used to maintain stable xmi.id values on output.
* <li>Handles search special processing for profiles including the search list
* of directories which an be used to look them up.
* <li>Resolves a System ID to a fully specified URL which can be used by MDR
* to open and read the referenced content. The standard MDR resolver is
* extended to support that "jar:" protocol for URLs, allowing it to handle
* multi-file Zip/jar archives contained a set of models. The method
* <code>toUrl</code> and supporting methods and fields was copied from the
* AndroMDA 3.1 implementation
* (org.andromda.repositories.mdr.MDRXmiReferenceResolverContext) by Ludo
* (rastaman).
* </nl>
* <p>
* NOTE: This is not a standalone implementation of the reference resolver since
* it depends on extending the specific MDR implementation.
*
* @author Tom Morris
*
*/
class XmiReferenceResolverImpl extends XmiContext {
private static final Logger LOG =
Logger.getLogger(XmiReferenceResolverImpl.class.getName());
/**
* Map of href/id to object. IDs for top level document will have no
* leading URL piece while others will be in <url>#<id> form
*/
private Map<String, Map<String, Object>> idToObject =
Collections.synchronizedMap(new HashMap<String, Map<String, Object>>());
/**
* Map indexed by MOF ID containing XmiReference objects.
*/
private Map<String, XmiReference> mofidToXmiref;
/**
* System ID of top level document
*/
private String topSystemId;
/**
* Most recent system ID (public ID in our context) translated by toURL
*/
private Map<String, URL> pendingProfiles = new HashMap<String, URL>();
/**
* URI form of topSystemID for use in relativization.
*/
private URI baseUri;
/**
* The array of paths in which the models references in other models will be
* searched.
* Copied from AndroMDA 3.1 by Ludo (rastaman).
* see org.andromda.repositories.mdr.MDRXmiReferenceResolverContext
*/
private List<String> modulesPath = new ArrayList<String>();
/**
* Module to URL map to cache things we've already found.
* Copied from AndroMDA 3.1 by Ludo (rastaman).
*
* see org.andromda.repositories.mdr.MDRXmiReferenceResolverContext
*/
private Map<String, URL> urlMap = new HashMap<String, URL>();
/**
* Mapping from absolute resolved URL to the original SystemID
* that was read from the input file. We'll preserve this mapping when
* we write things back out again.
*/
private Map<String, String> reverseUrlMap = new HashMap<String, String>();
/**
* True if top level file is a profile/readonly
*/
private boolean profile;
/**
* Mapping from public ID to system ID for files which have been read.
*/
private Map<String, String> public2SystemIds;
private String modelPublicId;
private MDRModelImplementation modelImpl;
/**
* Constructor.
* @param systemId
* @see org.netbeans.lib.jmi.xmi.XmiContext#XmiContext(javax.jmi.reflect.RefPackage[], org.netbeans.api.xmi.XMIInputConfig)
* (see also {link org.netbeans.api.xmi.XMIReferenceResolver})
*/
// CHECKSTYLE:OFF - ignore too many parameters since API is fixed by MDR
XmiReferenceResolverImpl(RefPackage[] extents, XMIInputConfig config,
Map<String, XmiReference> objectToXmiref,
Map<String, String> publicIds,
Map<String, Map<String, Object>> idToObject,
List<String> searchDirs,
boolean isProfile, String publicId, String systemId,
MDRModelImplementation modelImplementation) {
// CHECKSTYLE:ON
super(extents, config);
modelImpl = modelImplementation;
mofidToXmiref = objectToXmiref;
modulesPath = searchDirs;
profile = isProfile;
public2SystemIds = publicIds;
this.idToObject = idToObject;
modelPublicId = publicId;
if (isProfile) {
if (publicId == null) {
LOG.log(Level.WARNING, "Profile load with null public ID. Using system ID - "
+ systemId);
modelPublicId = publicId = systemId;
}
if (public2SystemIds.containsKey(modelPublicId)) {
if (systemId.equals(public2SystemIds.get(publicId))) {
LOG.log(Level.WARNING, "Loaded profile is being re-read "
+ "publicId = \"" + publicId + "\"; systemId = \""
+ systemId + "\".");
} else {
LOG.log(Level.WARNING, "Profile with the duplicate publicId "
+ "is being loaded! publicId = \"" + publicId
+ "\"; existing systemId = \""
+ public2SystemIds.get(publicId)
+ "\"; new systemId = \"" + systemId + "\".");
}
}
public2SystemIds.put(publicId, systemId);
}
}
/**
* Save registered ID in our object map.
*
* @param systemId
* URL of XMI field
* @param xmiId
* xmi.id string for current object
* @param object
* referenced object
*/
@Override
public void register(final String systemId, final String xmiId,
final RefObject object) {
LOG.log(Level.FINE,
"Registering XMI ID {0} in system ID {1} to object with MOF ID {2}",
new Object[]{xmiId, systemId, object.refMofId()});
if (topSystemId == null) {
topSystemId = systemId;
try {
baseUri = new URI(
systemId.substring(0, systemId.lastIndexOf('/') + 1));
} catch (URISyntaxException e) {
LOG.log(Level.WARNING, "Bad URI syntax for base URI from XMI document "
+ systemId, e);
baseUri = null;
}
LOG.log(Level.FINE, "Top system ID set to {0}", topSystemId);
}
String resolvedSystemId = systemId;
if (profile && systemId.equals(topSystemId)) {
resolvedSystemId = modelPublicId;
} else if (reverseUrlMap.get(systemId) != null) {
resolvedSystemId = reverseUrlMap.get(systemId);
} else {
LOG.log(Level.FINE, "Unable to map systemId - {0}", systemId);
}
RefObject o = getReferenceInt(resolvedSystemId, xmiId);
if (o == null) {
if (mofidToXmiref.containsKey(object.refMofId())) {
XmiReference ref = mofidToXmiref.get(object.refMofId());
// For now just skip registering this and ignore the request,
// but the real issue is that MagicDraw serializes the same
// object in two different composition associations, first in
// the referencing file and second in the referenced file
LOG.log(Level.FINE, "register called twice for the same object "
+ "- ignoring second");
LOG.log(Level.FINE, " - first reference = {0}#{1}", new Object[]{ref.getSystemId(), ref.getXmiId()});
LOG.log(Level.FINE, " - 2nd reference = {0}#{1}", new Object[]{systemId, xmiId});
LOG.log(Level.FINE, " - resolved system id = {0}", resolvedSystemId );
} else {
registerInt(resolvedSystemId, xmiId, object);
super.register(resolvedSystemId, xmiId, object);
}
} else {
if (o.equals(object)) {
// Object from a different file, register with superclass so it
// can resolve all references
super.register(resolvedSystemId, xmiId, object);
} else {
LOG.log(Level.SEVERE, "Collision - multiple elements with same xmi.id : "
+ xmiId);
throw new IllegalStateException(
"Multiple elements with same xmi.id");
}
}
}
private RefObject getReferenceInt(String docId, String xmiId) {
Map<String, Object> map = idToObject.get(docId);
if (map != null) {
RefObject result = (RefObject) map.get(xmiId);
if (result == null ) {
LOG.log(Level.FINE, "No internal reference for - {0}#{1}", new Object[]{docId, xmiId});
}
return result;
}
return null;
}
private void registerInt(String docId, String xmiId, RefObject object) {
Map<String, Object> map = idToObject.get(docId);
if (map == null) {
map = new HashMap<String, Object>();
idToObject.put(docId,map);
}
map.put(xmiId, object);
mofidToXmiref.put(object.refMofId(), new XmiReference(docId, xmiId));
}
/*
* @see org.netbeans.lib.jmi.xmi.XmiContext#getReference(java.lang.String, java.lang.String)
*/
public RefObject getReference (String docId, String xmiId) {
RefObject ro = getReferenceInt(docId, xmiId);
if (ro == null && !idToObject.containsKey(docId)) {
ro = super.getReference(docId, xmiId);
}
if (ro == null) {
// TODO: Distinguish between deferred resolution and things which
// are unresolved at end of load and should be reported to user.
LOG.log(Level.SEVERE, "Failed to resolve " + docId + "#" + xmiId );
}
// TODO: Count/report unresolved references
return ro;
}
/**
* Return map of all registered objects for top level document.
*
* @return map of xmi.id to RefObject correspondences
*/
Map<String, Object> getIdToObjectMap() {
if (idToObject != null) {
return idToObject.get(topSystemId);
} else {
return null;
}
}
/**
* @return map of maps from xmi ID to object
*/
Map<String, Map<String, Object>> getIdToObjectMaps() {
return idToObject;
}
/**
* Reinitialize the object id maps to the empty state.
*/
void clearIdMaps() {
Map<String, Object> map = getIdToObjectMap();
if (map != null) {
map.clear();
}
mofidToXmiref.clear();
topSystemId = null;
}
/////////////////////////////////////////////////////
////////// Begin AndroMDA Code //////////////////////
/////////////////////////////////////////////////////
/**
* Convert a System ID from an HREF which may be relative or otherwise in
* need of resolution to an absolute URL.
*
* Copied from AndroMDA 3.1 by Ludo (rastaman)
* see @link org.andromda.repositories.mdr.MDRXmiReferenceResolverContext
* @see org.netbeans.lib.jmi.xmi.XmiContext#toURL(java.lang.String)
*/
@Override
public URL toURL(String systemId) {
LOG.log(Level.FINE,
"attempting to resolve Xmi Href --> {0}", systemId);
// TODO: Using just the last piece of the ID leaves the potential for
// name collisions if two linked files have the same name in different
// directories
final String suffix = getSuffix(systemId);
// if the model URL has a suffix of '.zip' or '.jar', get
// the suffix without it and store it in the urlMap
String exts = "\\.jar|\\.zip";
String suffixWithExt = suffix.replaceAll(exts, "");
URL modelUrl = urlMap.get(suffixWithExt);
// Several tries to construct a URL that really exists.
if (modelUrl == null) {
if (public2SystemIds.containsKey(systemId)) {
// If systemId is publicId previously mapped from a systemId,
// try to use the systemId.
modelUrl = getValidURL(public2SystemIds.get(systemId));
}
if (modelUrl == null) {
// If systemId is a valid URL, simply use it.
// TODO: This causes a network connection attempt for profiles
modelUrl = getValidURL(fixupURL(systemId));
}
if (modelUrl == null) {
// Try to find suffix in module list.
String modelUrlAsString = findModuleURL(suffix);
if (!(modelUrlAsString == null
|| "".equals(modelUrlAsString))) {
modelUrl = getValidURL(modelUrlAsString);
}
if (modelUrl == null) {
// search the classpath
modelUrl = findModelUrlOnClasspath(systemId);
}
if (modelUrl == null) {
// Give up and let superclass deal with it.
modelUrl = super.toURL(systemId);
}
}
// if we've found the module model, log it
// and place it in the map so we don't have to
// find it if we need it again.
if (modelUrl != null) {
LOG.log(Level.INFO, "Referenced model --> {0}", modelUrl);
urlMap.put(suffixWithExt, modelUrl);
pendingProfiles.put(systemId, modelUrl);
String relativeUri = systemId;
try {
if (baseUri != null) {
relativeUri = baseUri.relativize(modelUrl.toURI())
.toString();
LOG.log(Level.FINE, " system ID {0} modelUrl {1}\n relativized as {2}",
new Object[]{systemId, modelUrl, relativeUri});
} else {
relativeUri = systemId;
}
} catch (URISyntaxException e) {
LOG.log(Level.SEVERE, "Error relativizing system ID " + systemId, e);
relativeUri = systemId;
}
// TODO: Check whether this is really needed. I think it's
// left over from an incomplete understanding of the MagicDraw
// composition error problem - tfm
reverseUrlMap.put(modelUrl.toString(), relativeUri);
reverseUrlMap.put(systemId, relativeUri);
} else {
// TODO: We failed to resolve URL - signal error
}
}
return modelUrl;
}
/**
* Finds a module in the module search path.
* <p>
* Copied from AndroMDA 3.1 by Ludo (rastaman).
*
* see org.andromda.repositories.mdr.MDRXmiReferenceResolverContext
* @param moduleName
* the name of the module without any path
* @return the complete URL string of the module if found (null if not
* found)
*/
private String findModuleURL(String moduleName) {
if (modulesPath == null) {
return null;
}
LOG.log(Level.FINE, "findModuleURL: modulesPath.size() = {0}", modulesPath.size());
for (String moduleDirectory : modulesPath) {
File candidate = new File(moduleDirectory, moduleName);
LOG.log(Level.FINE,
"candidate {0} exists={1}",
new Object[]{candidate, candidate.exists()});
if (candidate.exists()) {
String urlString;
try {
urlString = candidate.toURI().toURL().toExternalForm();
} catch (MalformedURLException e) {
return null;
}
return fixupURL(urlString);
}
}
if (public2SystemIds.containsKey(moduleName)) {
LOG.log(Level.FINE,
"Couldn't find user model ({0}) in modulesPath, attempt to use a model stored within the zargo file.",
moduleName);
return moduleName;
}
return null;
}
/**
* Gets the suffix of the <code>systemId</code>.
* <p>
* Copied from AndroMDA 3.1 by Ludo (rastaman). see
* org.andromda.repositories.mdr.MDRXmiReferenceResolverContext
*
* @param systemId the system identifier.
* @return the suffix as a String.
*/
private String getSuffix(String systemId) {
int lastSlash = systemId.lastIndexOf("/");
if (lastSlash > 0) {
String suffix = systemId.substring(lastSlash + 1);
return suffix;
}
return systemId;
}
/**
* The suffixes to use when searching for referenced models on the
* classpath.
* <p>
* Copied from AndroMDA 3.1 by Ludo (rastaman).
* see org.andromda.repositories.mdr.MDRXmiReferenceResolverContext
*/
protected static final String[] CLASSPATH_MODEL_SUFFIXES =
new String[] {"xml", "xmi", };
/**
* Searches for the model URL on the classpath.
* <p>
* Copied from AndroMDA 3.1 by Ludo (rastaman).
*
* see org.andromda.repositories.mdr.MDRXmiReferenceResolverContext
*
* @param systemId
* the system identifier.
* @return the suffix as a String.
*/
private URL findModelUrlOnClasspath(String systemId) {
final String dot = ".";
String modelName = systemId;
if (public2SystemIds.containsKey(systemId)) {
modelName = public2SystemIds.get(systemId);
} else {
int filenameIndex = systemId.lastIndexOf("/");
if (filenameIndex > 0) {
modelName = systemId.substring(filenameIndex + 1, systemId
.length());
} else {
LOG.log(Level.WARNING, "Received systemId with no '/'" + systemId);
}
// remove the first prefix because it may be an archive
// (like magicdraw)
if (modelName.lastIndexOf(dot) > 0) {
modelName = modelName.substring(0, modelName.lastIndexOf(dot));
}
}
URL modelUrl = Thread.currentThread().getContextClassLoader()
.getResource(modelName);
// TODO: Not sure whether the above is better in some cases, but
// the code below is better for both Java Web Start and Eclipse.
if (modelUrl == null) {
modelUrl = this.getClass().getResource(modelName);
}
// TODO: Is this adequate for finding profiles in Java WebStart jars?
// - tfm
if (modelUrl == null) {
if (CLASSPATH_MODEL_SUFFIXES != null
&& CLASSPATH_MODEL_SUFFIXES.length > 0) {
for (String suffix : CLASSPATH_MODEL_SUFFIXES) {
LOG.log(Level.FINE,
"searching for model reference --> {0}", modelUrl);
modelUrl = Thread.currentThread().getContextClassLoader()
.getResource(modelName + dot + suffix);
if (modelUrl != null) {
break;
}
modelUrl = this.getClass().getResource(modelName);
if (modelUrl != null) {
break;
}
}
}
}
return modelUrl;
}
/**
* Returns a URL if the systemId is valid. Returns null otherwise. Catches
* exceptions as necessary.
* <p>
* Copied from AndroMDA 3.1 by Ludo (rastaman). See
* org.andromda.repositories.mdr.MDRXmiReferenceResolverContext
*
* @param systemId
* the system id
* @return the URL (if valid) or null
*/
private URL getValidURL(String systemId) {
InputStream stream = null;
URL url = null;
try {
url = new URL(systemId);
URLConnection connection = url.openConnection();
stream = connection.getInputStream();
// There is a design decision in java not to redirect
// automatically between http and https connections.
// This appeared as a problem when moving to github
// because the redirect from http://argouml.org then
// went to https://argouml*.github.io and suddenly
// the getInputStream succeeded for non-existing files
// since the redirect response doesn't throw an IOException.
if (connection instanceof HttpURLConnection) {
HttpURLConnection huc = (HttpURLConnection) connection;
if (huc.getResponseCode() / 100 == 3) {
String whereto = huc.getHeaderField("Location");
url = new URL(whereto);
connection = url.openConnection();
stream = connection.getInputStream();
}
} else if (connection instanceof HttpsURLConnection) {
HttpsURLConnection hsuc = (HttpsURLConnection) connection;
if (hsuc.getResponseCode() / 100 == 3) {
String whereto = hsuc.getHeaderField("Location");
url = new URL(whereto);
connection = url.openConnection();
stream = connection.getInputStream();
}
}
stream.close();
} catch (MalformedURLException e) {
url = null;
} catch (IOException e) {
url = null;
} finally {
stream = null;
}
return url;
}
/////////////////////////////////////////////////////
////////// End AndroMDA Code //////////////////////
/////////////////////////////////////////////////////
/**
* Fix up a file URL for a Zip file or Jar. Assume it is a single
* file archive with the entry name the same as the base name.
*/
private String fixupURL(String url) {
final String suffix = getSuffix(url);
if (suffix.endsWith(".zargo")) {
url = "jar:" + url + "!/"
+ suffix.substring(0, suffix.length() - 6) + ".xmi";
} else if (suffix.endsWith(".zip") || suffix.endsWith(".jar")) {
url = "jar:" + url + "!/"
+ suffix.substring(0, suffix.length() - 4);
}
return url;
}
@Override
public void readExternalDocument(String arg0) {
// We've got a profile read pending - handle it ourselves now
URL url = pendingProfiles.remove(arg0);
if (url != null) {
InputSource is = modelImpl.getInputSource(url);
is.setPublicId(arg0);
XmiReaderImpl reader = new XmiReaderImpl(modelImpl);
try {
reader.parse(is, true);
} catch (UmlException e) {
LOG.log(Level.SEVERE, "Error reading referenced profile " + arg0);
throw new XmiReferenceRuntimeException(arg0, e);
}
} else if (!(public2SystemIds.containsKey(arg0))) {
// Otherwise if it's not something we've already read, just
// punt to the super class.
try {
super.readExternalDocument(arg0);
} catch (DebugException e) {
// Unfortunately the MDR super implementation throws
// DebugException with just the message from the causing
// exception rather than nesting the exception itself, so
// we don't have all the information we'd like
LOG.log(Level.SEVERE, "Error reading external document " + arg0);
throw new XmiReferenceRuntimeException(arg0, e);
}
}
}
}