/* $Id: XmiReaderImpl.java 18631 2010-08-08 06:49:47Z linus $ ***************************************************************************** * Copyright (c) 2005,2010 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: * Bob Tarling * Tom Morris * euluis ***************************************************************************** * * Some portions of this file was previously release using the BSD License: */ // Copyright (c) 1996-2009 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.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import javax.jmi.reflect.InvalidObjectException; import javax.jmi.reflect.RefObject; import javax.jmi.reflect.RefPackage; import javax.jmi.xmi.MalformedXMIException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.sax.SAXTransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import org.apache.log4j.Logger; import org.argouml.model.UmlException; import org.argouml.model.XmiException; import org.argouml.model.XmiReader; import org.netbeans.api.mdr.MDRepository; import org.netbeans.api.xmi.XMIReader; import org.netbeans.api.xmi.XMIReaderFactory; import org.netbeans.lib.jmi.xmi.InputConfig; import org.netbeans.lib.jmi.xmi.UnknownElementsListener; import org.netbeans.lib.jmi.xmi.XMIHeaderConsumer; import org.omg.uml.UmlPackage; import org.openide.ErrorManager; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.XMLFilter; import org.xml.sax.XMLReader; /** * A wrapper around the genuine XmiReader that provides public access with no * knowledge of actual UML implementation. * * @author Bob Tarling */ class XmiReaderImpl implements XmiReader, UnknownElementsListener, XMIHeaderConsumer { /** * Logger. */ private static final Logger LOG = Logger.getLogger(XmiReaderImpl.class); private MDRModelImplementation modelImpl; private XmiReferenceResolverImpl resolver; /** * Flag indicating unknown element was found in XMI file. */ private boolean unknownElement; /** * Name of first unknown element found (if not a UML 1.3 name). */ private String unknownElementName; /** * Flag indicating that we think unknown element was due to a UML 1.3 file. */ private boolean uml13; /** * Elements to ignore errors on if they aren't found in the metamodel. */ private String[] ignoredElements = new String[] {}; /** * Flag indicating that we stripped at least one diagram during the import. */ private int ignoredElementCount; /** * String that we pulled from the header of the XMI file */ private String xmiHeader; /** * Constructor for XMIReader. * @param parentModelImplementation The ModelImplementation */ XmiReaderImpl(MDRModelImplementation parentModelImplementation) { modelImpl = parentModelImplementation; } public Collection parse(InputSource inputSource, boolean readOnly) throws UmlException { Collection<RefObject> newElements = Collections.emptyList(); String extentBase = inputSource.getPublicId(); if (extentBase == null) { extentBase = inputSource.getSystemId(); } if (extentBase == null) { extentBase = MDRModelImplementation.MODEL_EXTENT_NAME; } String extentName = extentBase; UmlPackage extent = (UmlPackage) modelImpl.getRepository().getExtent(extentName); int serial = 1; while (extent != null) { extentName = extentBase + " " + serial; serial++; extent = (UmlPackage) modelImpl.getRepository().getExtent( extentName); } extent = (UmlPackage) modelImpl.createExtent(extentName, readOnly); if (extent == null) { LOG.error("Failed to create extent " + extentName); } try { LOG.info("Loading to extent '" + extentName + "' " + extent); InputConfig config = new InputConfig(); config.setUnknownElementsListener(this); config.setUnknownElementsIgnored(true); String pId = inputSource.getPublicId(); String sId = modelImpl.getPublic2SystemIds().get(pId); if (sId != null) { if (sId.equals(inputSource.getSystemId())) { LOG.info("Attempt to reread profile - ignoring - " + "publicId = \"" + pId + "\"; systemId = \"" + sId + "\"."); return Collections.emptySet(); } else { throw new UmlException("Profile with the duplicate publicId " + "is being loaded! publicId = \"" + pId + "\"; existing systemId = \"" + modelImpl.getPublic2SystemIds().get(pId) + "\"; new systemId = \"" + sId + "\"."); } } resolver = new XmiReferenceResolverImpl(new RefPackage[] {extent}, config, modelImpl.getObjectToId(), modelImpl.getPublic2SystemIds(), modelImpl.getIdToObject(), modelImpl.getSearchPath(), readOnly, inputSource.getPublicId(), inputSource.getSystemId(), modelImpl); config.setReferenceResolver(resolver); config.setHeaderConsumer(this); XMIReader xmiReader = XMIReaderFactory.getDefault().createXMIReader(config); /* * MDR has a hardcoded printStackTrace on all exceptions, * even if they're caught, which is unsightly, so we handle * unknown elements ourselves rather than letting MDR throw * an exception for us to catch. * * org/netbeans/lib/jmi/util/Logger.java * * This can be uses to disable logging. Default output is * System.err * setProperty("org.netbeans.lib.jmi.Logger.fileName", "") * org.netbeans.mdr.Logger * * The property org.netbeans.lib.jmi.Logger controls the minimum * severity level for logging */ // Turn off NetBeans logging to System.err // System.setProperty("org.netbeans.lib.jmi.Logger.fileName", ""); // Set minimum severity level for MDR // System.setProperty("org.netbeans.lib.jmi.Logger", // Integer.toString(ErrorManager.INFORMATIONAL)); InputConfig config2 = (InputConfig) xmiReader.getConfiguration(); config2.setUnknownElementsListener(this); config2.setUnknownElementsIgnored(true); unknownElement = false; uml13 = false; ignoredElementCount = 0; // Disable event delivery during model load modelImpl.getModelEventPump().stopPumpingEvents(); try { String systemId = inputSource.getSystemId(); // If we've got a streaming input, copy it to make sure we'll // be able to rewind it if necessary if (inputSource.getByteStream() != null || inputSource.getCharacterStream() != null) { File file = copySource(inputSource); systemId = file.toURI().toURL().toExternalForm(); String publicId = inputSource.getPublicId(); inputSource = new InputSource(systemId); inputSource.setPublicId(publicId); } MDRepository repository = modelImpl.getRepository(); // Use a transaction to avoid the performance penalty (3x) of // MDR's autocommit mode repository.beginTrans(true); newElements = xmiReader.read(inputSource.getByteStream(), systemId, extent); // If a UML 1.3 file, attempt to upgrade it to UML 1.4 if (uml13) { // Roll back transaction from first attempt & start new one repository.endTrans(true); repository.beginTrans(true); // Clear the associated ID maps & reset starting collection resolver.clearIdMaps(); newElements = convertAndLoadUml13(inputSource.getSystemId(), extent, xmiReader, inputSource); } // Commit our transaction repository.endTrans(); } catch (Throwable e) { // Roll back transaction to remove any partial results read try { modelImpl.getRepository().endTrans(true); } catch (Throwable e2) { // Ignore any error. The transaction may already have // been unwound as part of exception processing by MDR } if (e instanceof MalformedXMIException) { throw (MalformedXMIException) e; } else if (e instanceof IOException) { throw (IOException) e; } else { // We shouldn't get here, but just in case... // We want a wide exception catcher to make sure our // transaction always gets ended e.printStackTrace(); throw new MalformedXMIException(); } } finally { modelImpl.getModelEventPump().startPumpingEvents(); } if (unknownElement) { modelImpl.deleteExtent(extent); throw new XmiException("Unknown element in XMI file : " + unknownElementName); } if (ignoredElementCount > 0) { LOG.warn("Ignored one or more elements from list " + ignoredElements); } } catch (MalformedXMIException e) { // If we can find a nested SAX exception, it will have information // on the line number, etc. ErrorManager.Annotation[] annotations = ErrorManager.getDefault().findAnnotations(e); for (ErrorManager.Annotation annotation : annotations) { Throwable throwable = annotation.getStackTrace(); if (throwable instanceof SAXParseException) { SAXParseException spe = (SAXParseException) throwable; throw new XmiException(spe.getMessage(), spe.getPublicId(), spe.getSystemId(), spe.getLineNumber(), spe.getColumnNumber(), e); } else if (throwable instanceof SAXException) { SAXException se = (SAXException) throwable; Exception e1 = se.getException(); if (e1 instanceof org.argouml.model.XmiReferenceRuntimeException) { String href = ((org.argouml.model.XmiReferenceRuntimeException) e1) .getReference(); throw new org.argouml.model.XmiReferenceException(href, e); } throw new XmiException(se.getMessage(), se); } } modelImpl.deleteExtent(extent); throw new XmiException(e); } catch (IOException e) { try { modelImpl.deleteExtent(extent); } catch (InvalidObjectException e2) { // Ignore if the extent never got created or has been deleted } throw new XmiException(e); } return newElements; } private Collection<RefObject> convertAndLoadUml13(String systemId, RefPackage extent, XMIReader xmiReader, InputSource input) throws FileNotFoundException, UmlException, IOException, MalformedXMIException { LOG.info("XMI file doesn't appear to be UML 1.4 - " + "attempting UML 1.3->UML 1.4 conversion"); final String[] transformFiles = new String[] { "NormalizeNSUML.xsl", "uml13touml14.xsl", }; unknownElement = false; // InputSource xformedInput = chainedTransform(transformFiles, pIs); InputSource xformedInput = serialTransform(transformFiles, input); xformedInput.setPublicId(input.getPublicId()); return xmiReader.read(xformedInput.getByteStream(), xformedInput .getSystemId(), extent); } /* * @see org.argouml.model.XmiReader#getXMIUUIDToObjectMap() */ public Map<String, Object> getXMIUUIDToObjectMap() { if (resolver != null) { return resolver.getIdToObjectMap(); } return null; } private static final String STYLE_PATH = "/org/argouml/model/mdr/conversions/"; /* * A near clone of this code works fine outside of ArgoUML, but throws a * null pointer exception during the transform when run within ArgoUML I * think it's something to do with the class libraries being used, but I * can't figure out what, so I've done a simpler, less efficient stepwise * translation below in serialTransform */ private InputSource chainedTransform(String[] styles, InputSource input) throws XmiException { SAXTransformerFactory stf = (SAXTransformerFactory) TransformerFactory.newInstance(); // TODO: Reconfigure exception handling to distinguish between errors // that are possible due to bad input data and those that represent // unexpected processing errors. try { // Set up reader to be first filter in chain SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser parser = spf.newSAXParser(); XMLReader last = parser.getXMLReader(); // Create filter for each style sheet and chain to previous // filter/reader for (int i = 0; i < styles.length; i++) { String xsltFileName = STYLE_PATH + styles[i]; URL xsltUrl = getClass().getResource(xsltFileName); if (xsltUrl == null) { throw new IOException("Error opening XSLT style sheet : " + xsltFileName); } StreamSource xsltStreamSource = new StreamSource(xsltUrl.openStream()); xsltStreamSource.setSystemId(xsltUrl.toExternalForm()); XMLFilter filter = stf.newXMLFilter(xsltStreamSource); filter.setParent(last); last = filter; } SAXSource transformSource = new SAXSource(last, input); // Create temporary file for output // TODO: we should be able to chain this directly to XMI reader File tmpFile = File.createTempFile("zargo_model_", ".xmi"); tmpFile.deleteOnExit(); StreamResult result = new StreamResult( new FileOutputStream(tmpFile)); Transformer transformer = stf.newTransformer(); transformer.transform(transformSource, result); return new InputSource(new FileInputStream(tmpFile)); } catch (SAXException e) { throw new XmiException(e); } catch (ParserConfigurationException e) { throw new XmiException(e); } catch (IOException e) { throw new XmiException(e); } catch (TransformerConfigurationException e) { throw new XmiException(e); } catch (TransformerException e) { throw new XmiException(e); } } private InputSource serialTransform(String[] styles, InputSource input) throws UmlException { SAXSource myInput = new SAXSource(input); SAXTransformerFactory stf = (SAXTransformerFactory) TransformerFactory.newInstance(); try { for (int i = 0; i < styles.length; i++) { // Set up source for style sheet String xsltFileName = STYLE_PATH + styles[i]; if (LOG.isInfoEnabled()) { LOG.info("Transforming with " + xsltFileName); } URL xsltUrl = getClass().getResource(xsltFileName); if (xsltUrl == null) { throw new UmlException("Error opening XSLT style sheet : " + xsltFileName); } StreamSource xsltStreamSource = new StreamSource(xsltUrl.openStream()); xsltStreamSource.setSystemId(xsltUrl.toExternalForm()); // Create & set up temporary output file File tmpOutFile = File.createTempFile("zargo_model_", ".xmi"); tmpOutFile.deleteOnExit(); StreamResult result = new StreamResult(new FileOutputStream( tmpOutFile)); // Create transformer and do transformation Transformer transformer = stf.newTransformer(xsltStreamSource); transformer.transform(myInput, result); LOG.info("Wrote converted XMI file - " + tmpOutFile + " converted using : " + xsltFileName); // Set up for next iteration myInput = new SAXSource(new InputSource(new FileInputStream( tmpOutFile))); myInput.setSystemId(tmpOutFile.toURI().toURL().toExternalForm()); } return myInput.getInputSource(); } catch (IOException e) { throw new UmlException(e); } catch (TransformerConfigurationException e) { throw new UmlException(e); } catch (TransformerException e) { throw new UmlException(e); } } private File copySource(InputSource input) throws IOException { byte[] buf = new byte[2048]; int len; // Create & set up temporary output file File tmpOutFile = File.createTempFile("zargo_model_", ".xmi"); tmpOutFile.deleteOnExit(); FileOutputStream out = new FileOutputStream(tmpOutFile); // TODO: Bob says - Coding by use of side effect here. // Maybe this should be done in a clearer way but it fixes // http://argouml.tigris.org/issues/show_bug.cgi?id=4978 // It seems that when loading an XMI that is not contained in a zip // file then the InputStream given as the argument to this method // can't be reused as it is at the end of the stream. In that case // systemId appears to be none-null at this stage. // So if systemId is not null we recreate the InputSource. String systemId = input.getSystemId(); if (systemId != null) { input = new InputSource(new URL(systemId).openStream()); } InputStream in = input.getByteStream(); while ((len = in.read(buf)) >= 0) { out.write(buf, 0, len); } out.close(); LOG.debug("Wrote copied XMI file to " + tmpOutFile); return tmpOutFile; } private static final String UML_13_ELEMENTS[] = { "TaggedValue.value", "TaggedValue.tag", "ModelElement.templateParameter2", "ModelElement.templateParameter3", "Classifier.structuralFeature", "Classifier.parameter", "AssociationEnd.type", "Node.resident", "ElementResidence.implementationLocation", "TemplateParameter.modelElement", "TemplateParameter.modelElement2", "Constraint.constrainedElement2", "UseCase.include2", "StateMachine.subMachineState", "ClassifierRole.message1", "ClassifierRole.message2", "Message.message3", "Message.message4", "ElementImport.modelElement", "ModelElement.elementResidence", "ModelElement.presentation", "ModelElement.supplierDependency", "ModelElement.templateParameter2", "ModelElement.templateParameter3", "ModelElement.binding", "GeneralizableElement.specialization", "Classifier.associationEnd", "Classifier.participant", "Operation.method", "Stereotype.extendedElement", "Stereotype.requiredTag", "TaggedValue.stereotype", "Signal.context", "Signal.reception", "Signal.sendAction", "UseCase.include2", "UseCase.extend2", "ExtensionPoint.extend", "Link.stimulus", "Instance.attributeLink", "Action.stimulus", "Event.state", "Event.transition", "Transition.state", "ClassifierRole.message1", "ClassifierRole.message2", "Message.message3", "Message.message4", "Action.state1", "Action.state2", "Action.state3", "Instance.stimulus1", "Instance.stimulus2", "Instance.stimulus3", }; public void elementFound(String name) { // Silently ignore anything specified by caller attempt to continue if (ignoredElements != null) { for (int i = 0; i < ignoredElements.length; i++) { if (name.equals(ignoredElements[i])) { ignoredElementCount++; return; } } } if (name.startsWith("Foundation.")) { uml13 = true; return; } for (int i = 0; i < UML_13_ELEMENTS.length; i++) { if (name.endsWith(UML_13_ELEMENTS[i])) { uml13 = true; return; } } unknownElement = true; if (unknownElementName == null) { unknownElementName = name; } LOG.error("Unknown XMI element named : " + name); } public boolean setIgnoredElements(String[] elementNames) { if (elementNames == null) { ignoredElements = new String[] {}; } else { ignoredElements = elementNames; } return true; } public String[] getIgnoredElements() { return ignoredElements; } public int getIgnoredElementCount() { return ignoredElementCount; } public String getTagName() { return "XMI"; } public void addSearchPath(String path) { modelImpl.addSearchPath(path); } public void removeSearchPath(String path) { modelImpl.removeSearchPath(path); } public List<String> getSearchPath() { return modelImpl.getSearchPath(); } public void consumeHeader(InputStream stream) { try { int length = stream.available(); byte[] bytes = new byte[length]; stream.read(bytes, 0, length); // we presume the stream is encoded using the default char encoding xmiHeader = new String(bytes); } catch (IOException e) { LOG.error("Exception reading XMI file header", e); } } public String getHeader() { return xmiHeader; } }