/* * JBoss, Home of Professional Open Source. * * See the LEGAL.txt file distributed with this work for information regarding copyright ownership and licensing. * * See the AUTHORS.txt file distributed with this work for a full listing of individual contributors. */ package org.teiid.designer.core.resource; import java.lang.reflect.Method; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import org.eclipse.core.runtime.IStatus; import org.eclipse.emf.common.util.BasicEList; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EAttribute; import org.eclipse.emf.ecore.EClassifier; import org.eclipse.emf.ecore.EDataType; import org.eclipse.emf.ecore.EFactory; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EPackage; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.EcorePackage; import org.eclipse.emf.ecore.InternalEObject; import org.eclipse.emf.ecore.impl.EcorePackageImpl; import org.eclipse.emf.ecore.xmi.XMIResource; import org.eclipse.emf.ecore.xmi.XMLHelper; import org.eclipse.emf.ecore.xmi.XMLResource; import org.eclipse.emf.ecore.xmi.impl.SAXXMIHandler; import org.eclipse.xsd.XSDPackage; import org.teiid.core.designer.id.IDGenerator; import org.teiid.core.designer.id.InvalidIDException; import org.teiid.core.designer.id.ObjectID; import org.teiid.core.designer.util.CoreArgCheck; import org.teiid.core.designer.util.ReflectionHelper; import org.teiid.designer.core.ModelerCore; import org.teiid.designer.core.util.DateUtil; import org.teiid.designer.metamodels.core.CorePackage; import org.teiid.designer.metamodels.core.ModelImport; import org.xml.sax.Attributes; /** * @since 8.0 */ public class EResourceXmiHandler extends SAXXMIHandler { private static final DateFormat[] DATE_FORMATS = {new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy"), //$NON-NLS-1$ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'.'SSSZ"), //$NON-NLS-1$ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'.'SSS"), //$NON-NLS-1$ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"), //$NON-NLS-1$ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm"), //$NON-NLS-1$ new SimpleDateFormat("yyyy-MM-dd")}; //$NON-NLS-1$ private final IDGenerator idGenerator; private final EResourceImpl eResource; private final Collection roots; private final Collection proxyResourceURIs; private final Collection modelImportsToConvert; private boolean isXsdResource; // private static final boolean DEBUG = false; /** * Constructor for EResourceXmiHandler. * * @param xmiResource * @param helper * @param options */ public EResourceXmiHandler( final XMIResource xmiResource, final XMLHelper helper, final Map options ) { super(xmiResource, helper, options); CoreArgCheck.isNotNull(xmiResource); CoreArgCheck.isInstanceOf(EResourceImpl.class, xmiResource); this.eResource = (EResourceImpl)xmiResource; this.idGenerator = IDGenerator.getInstance(); this.roots = new ArrayList(); this.proxyResourceURIs = new HashSet(); this.modelImportsToConvert = new HashSet(); this.isXsdResource = false; } /** * @see org.eclipse.emf.ecore.xmi.impl.XMLHandler#startElement(java.lang.String, java.lang.String, java.lang.String) */ @Override public void startElement( String uri, String localName, String name ) { super.startElement(uri, localName, name); } /** * @see org.eclipse.emf.ecore.xmi.impl.XMLHandler#createTopObject(java.lang.String,java.lang.String) */ @Override protected void createTopObject( final String prefix, final String name ) { if (isXsdPrefix(prefix)) { this.isXsdResource = true; } super.createTopObject(prefix, name); } /** * @see org.eclipse.emf.ecore.xmi.impl.XMLHandler#processTopObject(org.eclipse.emf.ecore.EObject) */ @Override protected void processTopObject( EObject object ) { super.processObject(object); this.roots.add(object); } /** * @see org.eclipse.emf.ecore.xmi.impl.SAXXMIHandler#handleObjectAttribs(org.eclipse.emf.ecore.EObject) */ @Override protected void handleObjectAttribs( final EObject obj ) { if (attribs != null) { InternalEObject internalEObject = (InternalEObject)obj; for (int i = 0, size = attribs.getLength(); i < size; ++i) { String qName = attribs.getQName(i); // If the xmi:id attribute is encountered ... if (qName.equals(ID_ATTRIB)) { this.xmlResource.setID(internalEObject, attribs.getValue(i)); // If the xmi:uuid attribute is encountered ... } else if (qName.equals(UUID_ATTRIB)) { ObjectID uuid = getObjectIDFromString(attribs.getValue(i)); CoreArgCheck.isNotNull(uuid); // Apply patch for defect 14449 uuid = patch_defect14449(obj, uuid, i); // Set the xmi:uuid value on the EObject instance ModelerCore.setObjectId(obj, uuid); // Add the EObject to the EResource local cache this.eResource.addToEObjectCache(obj, false); // If the xmi:href attribute is encountered ... } else if (qName.equals(XMLResource.HREF)) { String value = attribs.getValue(i); // If no resource URI prepends the href then the reference is assumed to be inside this resource if (value.startsWith("#")) { //$NON-NLS-1$ value = this.resourceURI.toString() + value; } handleProxy(internalEObject, value); // For all other attributes in which the prefix is not "xmlns" and is not one of the ignored features ... } else if (!qName.startsWith(XMLResource.XML_NS) && !notFeatures.contains(qName)) { EStructuralFeature feature = obj.eClass().getEStructuralFeature(qName); if (feature != null && feature.isChangeable()) { setAttribValue(obj, qName, attribs.getValue(i)); } // If we are setting a namespace URI on a new EPackage instance then register it if (obj instanceof EPackage && EcorePackage.eINSTANCE.getEPackage_NsURI().equals(feature)) { EPackage.Registry.INSTANCE.put(attribs.getValue(i), obj); } } } // First of two patches to convert the Core::ModelImport "path" feature value found in models // created prior to 4.4 to their new "modelLocation" value patchA_modelImport(obj, attribs); } } /** * @see org.eclipse.emf.ecore.xmi.impl.XMLHandler#handleProxy(org.eclipse.emf.ecore.InternalEObject, java.lang.String) * @since 4.3 */ @Override protected void handleProxy( InternalEObject proxy, String uriLiteral ) { super.handleProxy(proxy, uriLiteral); // Save the URI of the proxy's resource this.proxyResourceURIs.add(proxy.eProxyURI().trimFragment()); // If the resource is the built-in datatypes resource then we possibly need to convert // from a logical URI to a physical URI. For example, if the logical URI was // "http://www.w3.org/2001/XMLSchema#string" we need to remap this to the physical URI of // "file:/E:/.../cache/www.w3.org/2001/XMLSchema.xsd#//string;XSDSimpleTypeDefinition=7". URI physicalUri = null; if (eResource.getResourceSet() instanceof EResourceSetImpl) { EResourceSetImpl rs = (EResourceSetImpl)eResource.getResourceSet(); if (rs.getEObjectHrefConverter() != null) { physicalUri = rs.getEObjectHrefConverter().getPhysicalURI(proxy.eProxyURI()); } } if (physicalUri != null) { proxy.eSetProxyURI(physicalUri); } } /** * @see org.eclipse.emf.ecore.xmi.impl.XMLHandler#setFeatureValue(org.eclipse.emf.ecore.EObject, * org.eclipse.emf.ecore.EStructuralFeature, java.lang.Object, int) */ @Override protected void setFeatureValue( EObject object, EStructuralFeature feature, Object value, int position ) { // If the object for which the value is being set in XMLSchema model ... if (this.isXsdResource || isXsdPrefix(object.eClass().getEPackage().getNsPrefix())) { setValue(object, feature, value, position); return; } // If the feature has a datatype of java.util.Date then we need to ensure that the format of the // value adheres to one of the accepted formats in EFactoryImpl.EDATE_FORMATS. if (feature instanceof EAttribute && value instanceof String) { if (Date.class.equals(((EAttribute)feature).getEAttributeType().getInstanceClass())) { value = convertDateFormat((String)value); } } // Process as a normal feature value super.setFeatureValue(object, feature, value, position); } /** * @see org.eclipse.emf.ecore.xmi.impl.XMLHandler#createObjectFromFactory(org.eclipse.emf.ecore.EFactory, java.lang.String) */ @Override protected EObject createObjectFromFactory( EFactory factory, String typeName ) { EObject newObject = null; if (factory != null) { // Get the eProxy from the EResource local cache. If one is found, // as a side-effect, the eProxy is removed from that cache. newObject = findEProxy(); // If an eProxy could not be found then create a new EObject if (newObject == null) { newObject = helper.createObject(factory, helper.getType(factory, typeName)); } if (newObject != null) { if (disableNotify) { newObject.eSetDeliver(false); } handleObjectAttribs(newObject); // Fix for defect 12764. Ensure that any EObject instances from the // "http://www.eclipse.org/emf/2002/Ecore" metamodel are resolved immediately newObject = patch_defect12764(newObject, attribs); } } return newObject; } /** * @see org.eclipse.emf.ecore.xmi.impl.XMLHandler#endDocument() */ @Override public void endDocument() { // Execute XSDSchema.update() on any XSDSchema instances found as top level objects if (this.isXsdResource) { final List topObjects = this.xmlResource.getContents(); for (Iterator iter = topObjects.iterator(); iter.hasNext();) { updateSchema((EObject)iter.next()); } } this.eResource.getContents().addAll(this.roots); // This second patch reconciles the "modelLocation" value that was simply transfered from // / the "path" value in the first patch to its correct relative URI path patchB_modelImport(); super.endDocument(); } protected EObject findEProxy() { for (int i = 0, size = attribs.getLength(); i < size; ++i) { String qName = attribs.getQName(i); // If the xmi:uuid attribute is encountered ... if (qName.equals(UUID_ATTRIB)) { ObjectID uuid = getObjectIDFromString(attribs.getValue(i)); CoreArgCheck.isNotNull(uuid); EObject eProxy = this.eResource.findInEProxyCache(uuid); // Found the eProxy so remove it from the map and unset the eProxy URI if (eProxy != null) { this.eResource.removeFromEProxyCache(uuid); ((InternalEObject)eProxy).eSetProxyURI(null); } return eProxy; } } return null; } protected ObjectID getObjectIDFromString( final String uuidString ) { if (uuidString == null || uuidString.length() == 0) { return null; } try { return IDGenerator.getInstance().stringToObject(uuidString); } catch (InvalidIDException e) { // do nothing ... } return null; } /** * For a newly created EObject along with its associated attributes, check that EObject has an "href" attribte to a * "http://www.eclipse.org/emf/2002/Ecore" instance and resolve it immediately, otherwise return the original object. Fix for * defect 12764. */ protected EObject patch_defect12764( final EObject obj, final Attributes attribs ) { if (attribs != null) { for (int i = 0, size = attribs.getLength(); i < size; ++i) { String qName = attribs.getQName(i); String value = attribs.getValue(i); // If the xmi:href attribute is encountered ... if (qName.equals(XMLResource.HREF)) { URI uri = URI.createURI(value); if (EcorePackage.eNS_URI.equals(uri.trimFragment().toString())) { EcorePackageImpl.init(); } } } } return obj; } /** * Fix to defect 14449, where Core::ModelImport objects were created with the same UUID as the model that they reference. This * fix only addresses the side effect of the defect in existing models; see a little lower in this method for the fix so that * we don't reset it upon reading in (the original cause of the problem) * <p> * Example shown below in which the EObject xmi:uuid is the same as the "uuid" feature value. The "uuid" feature value * represents the UUID of the model being referenced by the import. <modelImports * xmi:uuid="mmuuid:bfa9eec0-c497-1fa8-a032-93e40f299f77" uuid="mmuuid:bfa9eec0-c497-1fa8-a032-93e40f299f77" ... * </p> * * @param obj EObject being processed * @param objUuid current UUID of the EObject being processed * @param attribsIndex current index into the Attributes collection * @return new ObjectID instance if the fix applies to this EObject, otherwise objUuid is returned * @since 4.3 */ protected ObjectID patch_defect14449( final EObject obj, final ObjectID objUuid, final int attribsIndex ) { CoreArgCheck.isTrue(attribsIndex >= 0 && attribsIndex < attribs.getLength(), "Attributes Index " + attribsIndex + " out of range"); //$NON-NLS-1$ //$NON-NLS-2$ CoreArgCheck.isTrue(attribs.getQName(attribsIndex).equals(UUID_ATTRIB), "QName " + attribs.getQName(attribsIndex) + " does not match " + UUID_ATTRIB); //$NON-NLS-1$ //$NON-NLS-2$ if (obj instanceof ModelImport) { final ModelImport modelImport = (ModelImport)obj; // The stringified UUID of the xmi:uuid attribute final String uuidString = attribs.getValue(attribsIndex); // Get the UUID of the referenced model (first on the object instance) ... String uuidOfRefedModel = modelImport.getUuid(); if (uuidOfRefedModel == null) { // That attribute hasn't yet been read in, so look in attributes that haven't yet been processed ... final String uuidFeatureName = CorePackage.eINSTANCE.getModelImport_Uuid().getName(); for (int k = (attribsIndex + 1); k < attribs.getLength(); ++k) { // start at the next unread attribute!!! String qName = attribs.getQName(k); if (qName.equals(uuidFeatureName)) { uuidOfRefedModel = attribs.getValue(k); } } } // Check if the xmi:uuid of the EObject is the same as the "uuid" feature value representing // the model referenced by the ModelImport. If they are the same then the EObject xmi:uuid // needs to be recreated. [Note: both values use the "mmuuid:" form, so okay to compare // the value strings] if (uuidString.equals(uuidOfRefedModel)) { return this.idGenerator.create(); } } return objUuid; } /** * First of two patches to convert the Core::ModelImport "path" feature value found in models created prior to 4.4 to their * new "modelLocation" value. In 4.4, the "path" feature on Core::ModelImport was changed to be transient and volatile to be * replaced by a new feature, "modelLocation". The new feature stores the relative URI path of the referenced resource instead * of the "path" relative to the workspace root which assumed an Eclipse runtime workspace. This first patch simply transfers * the "path" value to the new "modelLocation" feature. */ protected EObject patchA_modelImport( final EObject obj, final Attributes attribs ) { if (obj instanceof ModelImport && attribs != null) { for (int i = 0, size = attribs.getLength(); i < size; ++i) { String qName = attribs.getQName(i); String value = attribs.getValue(i); if (qName.equals("path")) { //$NON-NLS-1$ ((ModelImport)obj).setModelLocation(value); this.modelImportsToConvert.add(obj); } } } return obj; } /** * Second of two patches to convert the Core::ModelImport "path" feature value found in models created prior to 4.4 to their * new "modelLocation" value. In 4.4, the "path" feature on Core::ModelImport was changed to be transient and volatile to be * replaced by a new feature, "modelLocation". The new feature stores the relative URI path of the referenced resource instead * of the "path" relative to the workspace root which assumed an Eclipse runtime workspace. This second patch reconciles the * "modelLocation" value that was simply transfered from the "path" value in the first patch to its correct relative URI path */ protected void patchB_modelImport() { // If no ModelImport conversion work is required, return if (this.modelImportsToConvert.isEmpty()) { return; } URI eResourceURI = this.eResource.getURI(); // Pre-process the collection of proxy resource URIs ... removeBadProxyResourceUris(this.proxyResourceURIs); // Reconcile the workspace relative paths, stored in the "modelLocation" feature, // against the resource URIs of the proxies Collection unconvertedImports = new HashSet(this.modelImportsToConvert); for (Iterator i = this.modelImportsToConvert.iterator(); i.hasNext();) { ModelImport modelImport = (ModelImport)i.next(); String modelLocation = modelImport.getModelLocation().toLowerCase(); for (Iterator j = this.proxyResourceURIs.iterator(); j.hasNext();) { URI importURI = (URI)j.next(); String uriString = URI.decode(importURI.toString()).toLowerCase(); // If the URI of the proxy resource is a logical URI of the form "http://..." then ignore it if (uriString.startsWith("http") && !uriString.endsWith("xmi")) { //$NON-NLS-1$ //$NON-NLS-2$ continue; } // Match the modelLocation, currently of the form "/project/.../model.xmi" to // a resource URI of the form "file:/C:/.../project/.../model.xmi" if (uriString.endsWith(modelLocation)) { boolean deresolve = (eResourceURI != null && !eResourceURI.isRelative() && eResourceURI.isHierarchical()); if (deresolve && !importURI.isRelative()) { URI deresolvedURI = importURI.deresolve(eResourceURI, true, true, false); if (deresolvedURI.hasRelativePath()) { importURI = deresolvedURI; } } modelImport.setModelLocation(URI.decode(importURI.toString())); unconvertedImports.remove(modelImport); break; } } } // Reconcile the workspace relative paths, stored in the "modelLocation" feature, // against the model names found in the resource URIs of the proxies for (Iterator i = unconvertedImports.iterator(); i.hasNext();) { ModelImport modelImport = (ModelImport)i.next(); String modelLocation = removeProjectNameFromLocation(modelImport.getModelLocation()).toLowerCase(); for (Iterator j = this.proxyResourceURIs.iterator(); j.hasNext();) { URI importURI = (URI)j.next(); String uriString = URI.decode(importURI.toString()).toLowerCase(); // If the URI of the proxy resource is a logical URI of the form "http://..." then ignore it if (uriString.startsWith("http") && !uriString.endsWith("xmi")) { //$NON-NLS-1$ //$NON-NLS-2$ continue; } // Match the modelLocation, currently a project relative path, to a resource URI if (uriString.endsWith(modelLocation)) { boolean deresolve = (eResourceURI != null && !eResourceURI.isRelative() && eResourceURI.isHierarchical()); if (deresolve && !importURI.isRelative()) { URI deresolvedURI = importURI.deresolve(eResourceURI, true, true, false); if (deresolvedURI.hasRelativePath()) { importURI = deresolvedURI; } } modelImport.setModelLocation(URI.decode(importURI.toString())); i.remove(); break; } } } // Remove any model imports that could not be matched to a proxy resource for (Iterator i = unconvertedImports.iterator(); i.hasNext();) { ModelImport modelImport = (ModelImport)i.next(); modelImport.setModel(null); } } protected String removeProjectNameFromLocation( final String location ) { String newLocation = location; URI uri = URI.createURI(location); if (uri.segmentCount() > 1) { StringBuffer sb = new StringBuffer(location.length()); String[] segments = uri.segments(); for (int i = 1; i != segments.length; ++i) { sb.append("/"); //$NON-NLS-1$ sb.append(segments[i]); } newLocation = sb.toString(); } return newLocation; } /** * If any of the resource URIs begin with '/' then it may represent a bad href (Eclipse workspace relative path of the form * "/project/.../model.xmi") which are sometimes found in old model files. Check if the collection contains the correct file * URI so that the bad one can be removed */ protected void removeBadProxyResourceUris( final Collection proxyResourceUris ) { if (proxyResourceUris == null || proxyResourceUris.isEmpty()) { return; } // Collect all the URIs that are identified as being bad and // remove them from the collection of proxy resource URIs final List badUris = new ArrayList(proxyResourceUris.size()); for (Iterator i = proxyResourceUris.iterator(); i.hasNext();) { URI uri = (URI)i.next(); String uriString = uri.toString(); if (uriString.charAt(0) == '/') { badUris.add(uri); i.remove(); } } if (badUris.isEmpty()) { return; } // Check the collection of remaining proxy resource URIs attempting to // match the bad Eclipse workspace relative path to a good file URI path. // If a match is not found then re-add the bad path for (Iterator i = proxyResourceUris.iterator(); i.hasNext();) { URI uri = (URI)i.next(); String uriString = URI.decode(uri.toString()).toLowerCase(); for (Iterator j = badUris.iterator(); j.hasNext();) { URI badUri = (URI)j.next(); String badUriString = URI.decode(badUri.toString()).toLowerCase(); if (uriString.endsWith(badUriString)) { j.remove(); } } } if (!badUris.isEmpty()) { proxyResourceUris.addAll(badUris); } } protected void setValue( EObject object, EStructuralFeature feature, Object value, int position ) { int kind = helper.getFeatureKind(feature); switch (kind) { case XMLHelper.DATATYPE_SINGLE: case XMLHelper.DATATYPE_IS_MANY: EClassifier eMetaObject = feature.getEType(); EDataType eDataType = (EDataType)eMetaObject; EFactory eFactory = eDataType.getEPackage().getEFactoryInstance(); if (kind == XMLHelper.DATATYPE_IS_MANY) { BasicEList list = (BasicEList)object.eGet(feature); if (position == -2) { for (StringTokenizer stringTokenizer = new StringTokenizer((String)value, " "); //$NON-NLS-1$ stringTokenizer.hasMoreTokens();) { String token = stringTokenizer.nextToken(); list.addUnique(eFactory.createFromString(eDataType, token)); } // Make sure that the list will appear to be set to be empty. if (list.isEmpty()) { list.clear(); } } else if (value == null) { list.addUnique(null); } else { list.addUnique(eFactory.createFromString(eDataType, (String)value)); } } else if (value == null) { object.eSet(feature, null); } else { object.eSet(feature, eFactory.createFromString(eDataType, (String)value)); } break; case XMLHelper.IS_MANY_ADD: case XMLHelper.IS_MANY_MOVE: BasicEList list = (BasicEList)object.eGet(feature); if (position == -1) { list.addUnique(value); } else if (position == -2) { list.clear(); } else if (kind == XMLHelper.IS_MANY_ADD) { list.addUnique(position, value); } else { list.move(position, value); } break; default: object.eSet(feature, value); } } protected boolean isXsdPrefix( final String prefix ) { if (prefix.equalsIgnoreCase(XSDPackage.eNS_PREFIX)) { return true; } return false; } protected String convertDateFormat( final String value ) { Date valueAsDate = null; for (int i = 0; i < DATE_FORMATS.length; ++i) { try { valueAsDate = DATE_FORMATS[i].parse(value); break; } catch (ParseException parseException) { // do nothing } } if (valueAsDate == null) { final String msg = ModelerCore.Util.getString("EResourceXmiHandler.Error_parsing_date_string", value); //$NON-NLS-1$ ModelerCore.Util.log(IStatus.ERROR, msg); return value; } return DateUtil.getDateAsString(valueAsDate); } /** * Update any XMLSchema entity found in the model file. By calling update() on a XMLSchema entity we are forcing a datatype * analysis and validation of the whole schema. This is necessary to ensure that datatypes defined within the schema are * properly constrained. * * @param topObject */ protected void updateSchema( final EObject topObject ) { // Create the arguments to the method ... final Object[] args = new Object[] {}; final Class xmlSchemaClass = topObject.getClass(); final ReflectionHelper helper = new ReflectionHelper(xmlSchemaClass); Method updateSchemaMethod = null; try { updateSchemaMethod = helper.findBestMethodOnTarget("update", args); //$NON-NLS-1$ } catch (SecurityException e) { final String msg = ModelerCore.Util.getString("EResourceXmiHandler.Error_executing_XSD_update_method", xmlSchemaClass); //$NON-NLS-1$ ModelerCore.Util.log(IStatus.ERROR, e, msg); } catch (NoSuchMethodException e) { // do nothing } // Execute the XSDSchema.update() method if (updateSchemaMethod != null) { try { updateSchemaMethod.invoke(topObject, args); } catch (Throwable t) { final String msg = ModelerCore.Util.getString("EResourceXmiHandler.Error_executing_XSD_update_method", updateSchemaMethod); //$NON-NLS-1$ ModelerCore.Util.log(IStatus.ERROR, msg); } } } }