/** * <copyright> * * Copyright (c) 2010 SAP AG. * 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: * Reiner Hille-Doering (SAP AG) - initial API and implementation and/or initial documentation * * </copyright> */ package org.eclipse.bpmn2.util; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.xml.XMLConstants; import javax.xml.namespace.QName; import org.eclipse.bpmn2.Bpmn2Factory; import org.eclipse.bpmn2.Bpmn2Package; import org.eclipse.bpmn2.Definitions; import org.eclipse.bpmn2.Extension; import org.eclipse.bpmn2.Import; import org.eclipse.emf.common.CommonPlugin; import org.eclipse.emf.common.notify.NotificationChain; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EPackage; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.emf.ecore.util.ExtendedMetaData; import org.eclipse.emf.ecore.xmi.XMLHelper; import org.eclipse.emf.ecore.xmi.XMLLoad; import org.eclipse.emf.ecore.xmi.XMLResource; import org.eclipse.emf.ecore.xmi.XMLSave; import org.eclipse.emf.ecore.xmi.impl.SAXXMLHandler; import org.eclipse.emf.ecore.xmi.impl.XMLHelperImpl; import org.eclipse.emf.ecore.xmi.impl.XMLLoadImpl; import org.eclipse.emf.ecore.xmi.impl.XMLResourceImpl; import org.eclipse.emf.ecore.xmi.impl.XMLSaveImpl; import org.eclipse.xsd.ecore.XSDEcoreBuilder; import org.xml.sax.helpers.DefaultHandler; /** * <!-- begin-user-doc --> * The <b>Resource </b> associated with the package. * @implements Bpmn2Resource * <!-- end-user-doc --> * @see org.eclipse.bpmn2.util.Bpmn2ResourceFactoryImpl * @generated */ public class Bpmn2ResourceImpl extends XMLResourceImpl implements Bpmn2Resource { private QNameURIHandler uriHandler; private BpmnXmlHelper xmlHelper; // CHECK: make this optional (as it adds notification overhead) // ... or lazy (also works if added later on, because it attaches itself to the whole tree at once) protected Bpmn2OppositeReferenceAdapter oppositeReferenceAdapter = new Bpmn2OppositeReferenceAdapter(); public Bpmn2OppositeReferenceAdapter getOppositeReferenceAdapter() { return oppositeReferenceAdapter; } /** * Creates an instance of the resource. * <!-- begin-user-doc --> * <!-- end-user-doc --> * @param uri the URI of the new resource. * @generated NOT */ public Bpmn2ResourceImpl(URI uri) { super(uri); this.xmlHelper = new BpmnXmlHelper(this); this.uriHandler = new QNameURIHandler(xmlHelper); this.getDefaultLoadOptions().put(XMLResource.OPTION_URI_HANDLER, uriHandler); this.getDefaultSaveOptions().put(XMLResource.OPTION_URI_HANDLER, uriHandler); // only necessary if this resource will not be added to a ResourceSet instantly this.eAdapters().add(oppositeReferenceAdapter); } @Override public NotificationChain basicSetResourceSet(ResourceSet resourceSet, NotificationChain notifications) { if (resourceSet != null) resourceSet.eAdapters().add(oppositeReferenceAdapter); return super.basicSetResourceSet(resourceSet, notifications); } // This method is called by all save methods - save(Document,...), doSave(Writer/OutputStream, ...) - in superclasses. @Override protected XMLSave createXMLSave() { prepareSave(); return new XMLSaveImpl(createXMLHelper()) { @Override protected boolean shouldSaveFeature(EObject o, EStructuralFeature f) { if (Bpmn2Package.eINSTANCE.getDocumentation_Text().equals(f)) return false; if (Bpmn2Package.eINSTANCE.getFormalExpression_Body().equals(f)) return false; return super.shouldSaveFeature(o, f); } }; } /** * Prepares this resource for saving. * * Sets all ID attributes of contained and referenced objects * that are not yet set, to a generated UUID. */ protected void prepareSave() { EObject cur; Definitions thisDefinitions = ImportHelper.getDefinitions(this); for (Iterator<EObject> iter = getAllContents(); iter.hasNext();) { cur = iter.next(); setIdIfNotSet(cur); for (EObject referenced : cur.eCrossReferences()) { setIdIfNotSet(referenced); if (thisDefinitions != null) { Resource refResource = referenced.eResource(); if (refResource != null && refResource != this) { createImportIfNecessary(thisDefinitions, refResource); } } } } } /** * Set the ID attribute of cur to a generated ID, if it is not already set. * @param obj The object whose ID should be set. */ protected static void setIdIfNotSet(EObject obj) { if (obj.eClass() != null) { EStructuralFeature idAttr = obj.eClass().getEIDAttribute(); if (idAttr != null && !obj.eIsSet(idAttr)) { obj.eSet(idAttr, EcoreUtil.generateUUID()); } } } /** * Looks for an import of the referenced resource from the given definitions object. * If none is found, the method creates a new import element. * @param definitions The model that references an object contained in <code>reference</code> * and thus needs an import element to <code>reference</code>. * @param referenced The resource which needs to be imported into <code>definitions</code>. */ protected void createImportIfNecessary(Definitions definitions, Resource referenced) { if (ImportHelper.findImportForLocation(definitions, referenced.getURI()) == null) { URI referencingURI = ImportHelper.makeURICanonical(definitions.eResource().getURI()); URI referencedURI = ImportHelper.makeURICanonical(referenced.getURI()); Definitions importedDef = ImportHelper.getDefinitions(referenced); // only handle BPMN imports (with declared target namespace) if (importedDef != null && importedDef.getTargetNamespace() != null) { Import newImport = Bpmn2Factory.eINSTANCE.createImport(); newImport.setImportType(NamespaceHelper.xmiToXsdNamespaceUri(Bpmn2Package.eNS_URI)); newImport.setNamespace(importedDef.getTargetNamespace()); // Counterpart: location.resolve(referencingURI) == referencedURI ! newImport.setLocation(referencedURI.deresolve(referencingURI).toString()); definitions.getImports().add(newImport); } } } /** * We must override this method for having an own XMLHandler */ @Override protected XMLLoad createXMLLoad() { return new XMLLoadImpl(createXMLHelper()) { @Override protected DefaultHandler makeDefaultHandler() { return new BpmnXmlHandler(resource, helper, options); } }; } @Override protected XMLHelper createXMLHelper() { return this.xmlHelper; } /** * We need extend the standard SAXXMLHandler to hook into the handling of attribute references - which are no URIs but QNames. * @author Reiner Hille * */ protected static class BpmnXmlHandler extends SAXXMLHandler { public BpmnXmlHandler(XMLResource xmiResource, XMLHelper helper, Map<?, ?> options) { super(xmiResource, helper, options); } /** * Overridden to be able to convert QName references in attributes to URIs during load. * @param ids * In our case the parameter will contain exactly one QName that we resolve to URI. */ @Override protected void setValueFromId(EObject object, EReference eReference, String ids) { super.setValueFromId( object, eReference, eReference.isResolveProxies() ? ((QNameURIHandler) uriHandler) .convertQNameToUri(ids) : ids); } /** * Used from the <extension><definition> tag to load referenced extension schemes. * The extension scheme will be loaded and converted to EMF Ecore on the fly. * * @param id */ private EObject loadExtensionSchema(QName xsdQname) { EPackage extensionPackage = extendedMetaData.getPackage(xsdQname.getNamespaceURI()); if (extensionPackage == null) { try { @SuppressWarnings("unchecked") Class<XSDEcoreBuilder> theXSDEcoreBuilderClass = (Class<XSDEcoreBuilder>) CommonPlugin .loadClass("org.eclipse.xsd", "org.eclipse.xsd.ecore.XSDEcoreBuilder"); Constructor<XSDEcoreBuilder> theXSDEcoreBuilderConstructor = theXSDEcoreBuilderClass .getConstructor(new Class[] { ExtendedMetaData.class, Map.class }); Field theOptionField = theXSDEcoreBuilderClass .getField("OPTION_REUSE_REGISTERED_PACKAGES"); Object theXsdOption = theOptionField.get(null); URI location = urisToLocations.get(xsdQname.getNamespaceURI()); Map<Object, Object> options = new HashMap<Object, Object>(); options.put(theXsdOption, Boolean.TRUE); XSDEcoreBuilder builder = theXSDEcoreBuilderConstructor.newInstance( extendedMetaData, options); builder.generate(location); } catch (Exception e) { } } return extendedMetaData.getElement(xsdQname.getNamespaceURI(), xsdQname.getLocalPart()); } @Override public void endElement(String uri, String localName, String name) { // Detect Extension object EObject peekObject = objects.peek(); if (peekObject instanceof Extension) { Extension extension = (Extension) peekObject; if (extension.isMustUnderstand() && null != extension.getXsdDefinition()) { loadExtensionSchema(extension.getXsdDefinition()); } } super.endElement(uri, localName, name); } } /** * Extend XML Helper to gain access to the different XSD namespace handling features. * @author Reiner Hille * */ protected static class BpmnXmlHelper extends XMLHelperImpl { public BpmnXmlHelper(Bpmn2ResourceImpl resource) { super(resource); } private Definitions getDefinitions() { return ImportHelper.getDefinitions(getResource()); } /** * Checks if the given prefix is pointing to the current target namespace and thus is optional. * The method is called during load. * @param prefix The prefix or null, if no prefix is given (interpreted as default namespace). * @return <code>true</code>, if the namespace associated with the prefix equals the target namespace * of this Definitions. * If prefix is null or the empty string, then the default namespace is compared with the target namespace. * If prefix is null/empty and default namespace is not defined, <code>true</code> if the target namespace * is not defined either. * * <p> * The above rules describe a strict interpretation of the rules for QName resolution. * This method relaxes these rules and additionally returns <code>true</code> in the following cases: * <ul> * <li>prefix is null/empty, no default namespace (regardless of target namespace)</li> * <li>prefix is null/empty, default namespace is not {@linkplain ImportHelper#findImportForNamespace(Definitions, String) mapped by an import element}.</li> * </ul> * </p> */ public boolean isTargetNamespace(String prefix) { if (prefix == null) prefix = XMLConstants.DEFAULT_NS_PREFIX; final String prefixNs = this.getNamespaceURI(prefix); if (prefixNs == null) { if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) /* * The (empty) prefix points to {no namespace}, because no default namespace is defined. * This would be OK if target namespace is undefined as well (meaning {no namespace}). * * However, we employ a relaxed interpretation and do not require that * getDefinitions().getTargetNamespace() == null (i.e. target namespace == {no namespace}) * Every unprefixed QName is interpreted as local reference, if the default namespace is not defined. */ return true; // the non-empty prefix is not mapped to a namespace throw new IllegalArgumentException(String.format("The prefix '%s' is not valid.", prefix)); } // result with strict evaluation: return prefixNs.equals(getDefinitions().getTargetNamespace()) if (prefixNs.equals(getDefinitions().getTargetNamespace())) return true; else if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix) && ImportHelper.findImportForNamespace(getDefinitions(), prefixNs) == null) { // The default namespace is not mapped to a location by an import element. // Guess that the unprefixed QName should point to a local element (relaxed interpretation) // TODO: emit warning return true; } else return false; } /** * Looks up the given prefix in the list of BPMN import elements and returns - if found - the corresponding file location. * The method is called during load. * @param prefix * @return */ public URI getPathForPrefix(String prefix) { String ns = this.getNamespaceURI(prefix == null ? XMLConstants.DEFAULT_NS_PREFIX : prefix); if (ns != null) { Import imp = ImportHelper.findImportForNamespace(getDefinitions(), ns); if (imp != null) return URI.createURI(imp.getLocation()).resolve( ImportHelper.makeURICanonical(getResource().getURI())); else { return URI.createURI(ns); } } return URI.createURI(""); } /** * Partly stolen from XmlHelperImpl.setPrefixToNamespaceMap(). * Ensuring that namespace declaration is saved seems to be really tricky. * We will necessarily create a dummy package to ensure that later XmlSaveImpl.addNamespaceDeclarations() writes the ns declaration for us * @param namespace * @return */ private String getPrefixDuringSave(String namespace) { if (urisToPrefixes.containsKey(namespace)) return urisToPrefixes.get(namespace).get(0); EPackage ePackage = extendedMetaData.getPackage(namespace); if (ePackage == null) { ePackage = extendedMetaData.demandPackage(namespace); // This will internally create a nice prefix } String prefix; if (namespace.equals(getDefinitions().getTargetNamespace())) // try to use the default namespace (xmlns="...") for local references prefix = XMLConstants.DEFAULT_NS_PREFIX; else prefix = ePackage.getNsPrefix(); // Make prefix unique String originalPrefix = prefix + "_"; int discr = 0; while (prefixesToURIs.containsKey(prefix) && !prefixesToURIs.get(prefix).equals(namespace)) prefix = originalPrefix + discr++; // I'm not sure if the following code is needed, but I keep it to avoid inconsistencies if (!packages.containsKey(ePackage)) { packages.put(ePackage, prefix); } prefixesToURIs.put(prefix, namespace); return prefix; } /** * This is called on save to convert from a file-based URI to a namespace prefix. * It might be necessary to add a new namespace declaration to the file, if the * namespace was not known to far. * @param referenced Absolute or relative to current working directory. * @return */ public String getNsPrefix(URI referenced) { String ns = null; String prefix = ""; URI referencedAbs = ImportHelper.makeURICanonical(referenced); URI thisAbs = ImportHelper.makeURICanonical(getResource().getURI()); URI relativeToThis = referencedAbs.deresolve(thisAbs); if (relativeToThis.isEmpty()) // reference to local element ns = getDefinitions().getTargetNamespace(); else { Import impForRef = ImportHelper.findImportForLocation(getDefinitions(), referenced); if (impForRef != null) ns = impForRef.getNamespace(); } if (ns != null) { prefix = getPrefixDuringSave(ns); } return prefix; } } } //Bpmn2ResourceImpl