/** * Copyright 2005-2014 Restlet * * The contents of this file are subject to the terms of one of the following * open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can * select the license that you prefer but you may not use this file except in * compliance with one of these Licenses. * * You can obtain a copy of the Apache 2.0 license at * http://www.opensource.org/licenses/apache-2.0 * * You can obtain a copy of the EPL 1.0 license at * http://www.opensource.org/licenses/eclipse-1.0 * * See the Licenses for the specific language governing permissions and * limitations under the Licenses. * * Alternatively, you can obtain a royalty free commercial license with less * limitations, transferable or non-transferable, directly at * http://restlet.com/products/restlet-framework * * Restlet is a registered trademark of Restlet S.A.S. */ package org.restlet.ext.odata.internal.edm; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.restlet.data.MediaType; import org.restlet.data.Method; import org.restlet.ext.odata.Service; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; /** * Used to parse a metadata descriptor of a given OData service and generate the * associated object's tree. * * @author Thierry Boileau */ public class MetadataReader extends DefaultHandler { /** The list of defined states of this parser. */ private enum State { ASSOCIATION, ASSOCIATION_END, ASSOCIATION_SET, ASSOCIATION_SET_END, COMPLEX_TYPE, COMPLEX_TYPE_PROPERTY, DOCUMENTATION, ENTITY_CONTAINER, ENTITY_SET, ENTITY_TYPE, ENTITY_TYPE_KEY, ENTITY_TYPE_PROPERTY, FUNCTION, FUNCTION_IMPORT, NAVIGATION_PROPERTY, NONE, ON_DELETE, PARAMETER, REFERENTIAL_CONSTRAINT, SCHEMA, USING } /** List of possible values for the blob reference member. */ private final String[] blobEditRefValues = { "blobEditReference", "blobEditReferenceValue" }; /** List of possible values for the blob reference member. */ private final String[] blobRefValues = { "blobReference", "blobReferenceValue" }; /** The current association. */ private Association currentAssociation; /** The current association set. */ private AssociationSet currentAssociationSet; /** The current complex type. */ private ComplexType currentComplexType; /** The current entity container. */ private EntityContainer currentEntityContainer; /** The current entity type. */ private EntityType currentEntityType; /** The current functionn import. */ private FunctionImport currentFunctionImport; /** The metadata objet to update. */ private Metadata currentMetadata; /** The current schema. */ private Schema currentSchema; /** The registered collection of associations. */ private Map<String, NamedObject> registeredAssociations; /** The registered collection of complex types. */ private Map<String, NamedObject> registeredComplexTypes; /** The registered collection of entity containers. */ private Map<String, EntityContainer> registeredContainers; /** The registered collection of entity sets. */ private Map<String, NamedObject> registeredEntitySets; /** The registered collection of entity types. */ private Map<String, NamedObject> registeredEntityTypes; /** The registered collection of namespaces. */ private List<Namespace> registeredNamespaces; /** The current heap of states. */ private List<State> states; /** * Constructor. * * @param feed * The feed object to update during the parsing. */ public MetadataReader(Metadata metadata) { this.states = new ArrayList<State>(); pushState(State.NONE); this.currentMetadata = metadata; } /** * Pick up the first method name among a given list of proposed names. * Returns null if the proposed values are already in the given entity type. * * @param type * The entity type to check. * @param values * The list of proposed values. * @return A method name. */ private String chooseAttributeName(EntityType type, String[] values) { String result = null; int index = 0; // Check that one of the possible names is not already set. for (int i = 0; i < type.getProperties().size() && (index < values.length); i++) { Property property = type.getProperties().get(i); if (values[index].equals(property.getName())) { index++; } } for (int i = 0; i < type.getAssociations().size() && (index < values.length); i++) { NavigationProperty property = type.getAssociations().get(i); if (values[index].equals(property.getName())) { index++; } } if (index != values.length) { result = values[index]; } return result; } /** * Explores the given attributes in order to get a declared property * mapping. * * @param type * The type of the mapped property. * @param property * The property that declares the mapping (is null if the mapping * is declared on the entity type tag). * @param metadata * The metadata instance to update. * @param attributes * The XML attributes to parse. */ private void discoverMapping(EntityType type, Property property, Metadata metadata, Attributes attributes) { String contentKind = null; String nsPrefix = null; String nsUri = null; String propertyPath = null; String valuePath = null; boolean keepInContent = true; contentKind = attributes.getValue( Service.WCF_DATASERVICES_METADATA_NAMESPACE, "FC_ContentKind"); if (contentKind == null) { contentKind = "text"; } nsPrefix = attributes.getValue( Service.WCF_DATASERVICES_METADATA_NAMESPACE, "FC_NsPrefix"); nsUri = attributes.getValue( Service.WCF_DATASERVICES_METADATA_NAMESPACE, "FC_NsUri"); String str = attributes .getValue(Service.WCF_DATASERVICES_METADATA_NAMESPACE, "FC_KeepInContent"); if (str != null) { keepInContent = Boolean.parseBoolean(str); } if (property == null) { // mapping declared on the entity type, the "FC_SourcePath" // attribute is mandatory. propertyPath = attributes.getValue( Service.WCF_DATASERVICES_METADATA_NAMESPACE, "FC_SourcePath"); } else { propertyPath = property.getName(); } valuePath = attributes.getValue( Service.WCF_DATASERVICES_METADATA_NAMESPACE, "FC_TargetPath"); if (propertyPath != null && valuePath != null && !keepInContent) { // The mapping is really defined between a property and an XML // element, and the value is only available in a customized part of // the feed. if ((nsUri == null && nsPrefix == null) || (nsUri != null && nsPrefix != null)) { // The mapping is correctly declared (either in an ATOM or a // customized XML element). metadata.getMappings().add( new Mapping(type, nsPrefix, nsUri, propertyPath, valuePath, contentKind)); } } } @Override public void endDocument() throws SAXException { // Update references. for (Schema schema : currentMetadata.getSchemas()) { // - associations.ends.type for (Association association : schema.getAssociations()) { // association type for (AssociationEnd end : association.getEnds()) { end.setType((EntityType) resolve(end.getType(), registeredEntityTypes, schema)); } } for (EntityType entityType : schema.getEntityTypes()) { // entityType.key if (entityType.getKeys() != null) { List<Property> props = entityType.getKeys(); entityType.setKeys(new ArrayList<Property>()); for (Property prop : props) { for (Property property : entityType.getProperties()) { if (property.equals(prop)) { entityType.getKeys().add(property); break; } } } } // entityType.associations for (NavigationProperty navigationProperty : entityType .getAssociations()) { navigationProperty.setRelationship((Association) resolve( navigationProperty.getRelationship(), registeredAssociations, schema)); if (navigationProperty.getRelationship() != null) { // association's roles. for (AssociationEnd end : navigationProperty .getRelationship().getEnds()) { if (end.getRole().equals( navigationProperty.getFromRole().getRole())) { navigationProperty.setFromRole(end); } else if (end.getRole().equals( navigationProperty.getToRole().getRole())) { navigationProperty.setToRole(end); } } } else { navigationProperty.setFromRole(null); navigationProperty.setToRole(null); } } // entityType.baseType entityType .setBaseType((EntityType) resolve( entityType.getBaseType(), registeredEntityTypes, schema)); } for (ComplexType complexType : schema.getComplexTypes()) { // complexType.baseType complexType.setBaseType((ComplexType) resolve( complexType.getBaseType(), registeredComplexTypes, schema)); } } for (EntityContainer container : currentMetadata.getContainers()) { // - entityContainer.extended if (container.getExtended() != null) { container.setExtended(registeredContainers.get(container .getExtended().getName())); } for (AssociationSet associationSet : container.getAssociations()) { // - associationSet.association associationSet.setAssociation((Association) resolve( associationSet.getAssociation(), registeredAssociations, container.getSchema())); // - associationSet.ends.entitySet for (AssociationSetEnd end : associationSet.getEnds()) { for (EntitySet entitySet : container.getEntities()) { if (entitySet.equals(end.getType())) { end.setType(entitySet); break; } } } } // - entityContainer.entitySet.entityType for (EntitySet entitySet : container.getEntities()) { entitySet.setType((EntityType) resolve(entitySet.getType(), registeredEntityTypes, container.getSchema())); } // - entityContainer.functionImport.entitySet for (FunctionImport functionImport : container.getFunctionImports()) { functionImport.setEntitySet((EntitySet) resolve( functionImport.getEntitySet(), registeredEntitySets, container.getSchema())); } } for (Schema schema : currentMetadata.getSchemas()) { for (EntityType entityType : schema.getEntityTypes()) { // entityType.complexTypes for (ComplexProperty property : entityType .getComplexProperties()) { ComplexType type = (ComplexType) resolve( property.getComplexType(), registeredComplexTypes, schema); if (type != null) { property.setComplexType(type); } } } for (ComplexType complexType : schema.getComplexTypes()) { // entityType.complexTypes for (ComplexProperty property : complexType .getComplexProperties()) { ComplexType type = (ComplexType) resolve( property.getComplexType(), registeredComplexTypes, schema); if (type != null) { property.setComplexType(type); } } } } } @Override public void endElement(String uri, String localName, String name) throws SAXException { if ("schema".equalsIgnoreCase(localName)) { popState(); currentSchema = null; } else if ("using".equalsIgnoreCase(localName)) { popState(); } else if ("documentation".equalsIgnoreCase(localName)) { popState(); } else if ("entityType".equalsIgnoreCase(localName)) { if (currentEntityType.isBlob()) { String memberName = chooseAttributeName(currentEntityType, blobRefValues); if (memberName == null) { // Should not happen currentEntityType.setBlob(false); } else { Property property = new Property(memberName); currentEntityType.setBlobValueRefProperty(property); } // Sets the name of the property of the generated class that // contains the reference of resource that is able to update the // blob value. memberName = chooseAttributeName(currentEntityType, blobEditRefValues); if (memberName == null) { // Should not happen currentEntityType.setBlob(false); } else { Property property = new Property(memberName); currentEntityType.setBlobValueEditRefProperty(property); } } popState(); currentEntityType = null; } else if ("key".equalsIgnoreCase(localName)) { popState(); } else if ("property".equalsIgnoreCase(localName)) { popState(); } else if ("navigationProperty".equalsIgnoreCase(localName)) { popState(); } else if ("complexType".equalsIgnoreCase(localName)) { popState(); } else if ("association".equalsIgnoreCase(localName)) { popState(); currentAssociation = null; } else if ("end".equalsIgnoreCase(localName)) { popState(); } else if ("onDelete".equalsIgnoreCase(localName)) { popState(); } else if ("referentialConstraint".equalsIgnoreCase(localName)) { popState(); } else if ("functionImport".equalsIgnoreCase(localName)) { currentFunctionImport = null; popState(); } else if ("function".equalsIgnoreCase(localName)) { popState(); } else if ("entityContainer".equalsIgnoreCase(localName)) { popState(); currentEntityContainer = null; } else if ("entitySet".equalsIgnoreCase(localName)) { popState(); } else if ("associationSet".equalsIgnoreCase(localName)) { popState(); currentAssociationSet = null; } else if ("parameter".equalsIgnoreCase(localName)) { popState(); } } /** * Returns the current state when processing the document. * * @return the current state when processing the document. */ private State getState() { final State result = this.states.get(0); return result; } /** * Drops the current state from the stack and returns it. This state becomes * the former current state. * * @return the former current state. */ private State popState() { return this.states.remove(0); } /** * Adds the given state. * * @param state * The given state. */ private void pushState(State state) { this.states.add(0, state); } /** * Finds a namedObject inside a register. * * @param namedObject * The namedObject to find. * @param register * The register. * @param schema * The schema of the named object. * @return The namedObject if found inside the register, null otherwise. */ private NamedObject resolve(NamedObject namedObject, Map<String, NamedObject> register, Schema currentSchema) { NamedObject result = null; if (namedObject != null && namedObject.getName() != null) { String key = null; int index = namedObject.getName().lastIndexOf("."); if (index != -1) { // Objects are named via the namespace alias or full name String nsName = namedObject.getName().substring(0, index); for (Namespace namespace : registeredNamespaces) { if (nsName.equals(namespace.getAlias()) || nsName.equals(namespace.getName())) { key = namespace.getName() + namedObject.getName().substring(index); break; } } } else { key = currentSchema.getNamespace().getName() + "." + namedObject.getName(); } result = register.get(key); } return result; } @Override public void startDocument() throws SAXException { registeredComplexTypes = new HashMap<String, NamedObject>(); registeredEntityTypes = new HashMap<String, NamedObject>(); registeredAssociations = new HashMap<String, NamedObject>(); registeredEntitySets = new HashMap<String, NamedObject>(); registeredNamespaces = new ArrayList<Namespace>(); registeredContainers = new HashMap<String, EntityContainer>(); } @Override public void startElement(String uri, String localName, String name, Attributes attrs) throws SAXException { if ("schema".equalsIgnoreCase(localName)) { pushState(State.SCHEMA); currentSchema = new Schema(); this.currentMetadata.getSchemas().add(currentSchema); Namespace namespace = new Namespace(attrs.getValue("Namespace")); namespace.setAlias(attrs.getValue("Alias")); this.currentSchema.setNamespace(namespace); registeredNamespaces.add(namespace); } else if ("using".equalsIgnoreCase(localName)) { pushState(State.USING); Namespace namespace = new Namespace(attrs.getValue("Namespace")); namespace.setAlias(attrs.getValue("Alias")); this.currentSchema.getReferencedNamespaces().add(namespace); } else if ("documentation".equalsIgnoreCase(localName)) { pushState(State.DOCUMENTATION); } else if ("entityType".equalsIgnoreCase(localName)) { pushState(State.ENTITY_TYPE); currentEntityType = new EntityType(attrs.getValue("Name")); currentEntityType.setSchema(this.currentSchema); currentEntityType.setAbstractType(Boolean.parseBoolean(attrs .getValue("Abstract"))); currentEntityType.setBlob(Boolean.parseBoolean(attrs.getValue( Service.WCF_DATASERVICES_METADATA_NAMESPACE, "HasStream"))); String value = attrs.getValue("BaseType"); if (value != null) { currentEntityType.setBaseType(new EntityType(value)); } this.currentSchema.getEntityTypes().add(currentEntityType); // Check the declaration of a property mapping. discoverMapping(currentEntityType, null, currentMetadata, attrs); // register the new type. registeredEntityTypes.put(currentSchema.getNamespace().getName() + "." + currentEntityType.getName(), currentEntityType); } else if ("key".equalsIgnoreCase(localName)) { pushState(State.ENTITY_TYPE_KEY); } else if ("propertyRef".equalsIgnoreCase(localName)) { if (getState() == State.ENTITY_TYPE_KEY) { if (currentEntityType.getKeys() == null) { currentEntityType.setKeys(new ArrayList<Property>()); } currentEntityType.getKeys().add( new Property(attrs.getValue("Name"))); } } else if ("property".equalsIgnoreCase(localName)) { String type = attrs.getValue("Type"); Property property; if (type.toLowerCase().startsWith("edm.")) { property = new Property(attrs.getValue("Name")); property.setType(new Type(attrs.getValue("Type"))); } else { ComplexProperty p = new ComplexProperty(attrs.getValue("Name")); p.setComplexType(new ComplexType(attrs.getValue("Type"))); property = p; } property.setDefaultValue(attrs.getValue("Default")); // If no value is specified, the nullable facet defaults to true. // cf // http://www.odata.org/documentation/odata-v3-documentation/common-schema-definition-language-csdl/#531_The_edmNullable_Attribute String nullable = attrs.getValue("Nullable"); if (nullable == null) { property.setNullable(true); } else { property.setNullable(Boolean.parseBoolean(nullable)); } // ConcurrencyMode if ("fixed".equalsIgnoreCase(attrs.getValue("ConcurrencyMode"))) { property.setConcurrent(true); } else { property.setConcurrent(false); } property.setGetterAccess(attrs.getValue("GetterAccess")); property.setSetterAccess(attrs.getValue("SetterAccess")); String str = attrs.getValue( Service.WCF_DATASERVICES_METADATA_NAMESPACE, "MimeType"); if (str != null) { property.setMediaType(MediaType.valueOf(str)); } if (getState() == State.ENTITY_TYPE) { pushState(State.ENTITY_TYPE_PROPERTY); if (property instanceof ComplexProperty) { this.currentEntityType.getComplexProperties().add( (ComplexProperty) property); } else { this.currentEntityType.getProperties().add(property); } } else { pushState(State.COMPLEX_TYPE_PROPERTY); if (property instanceof ComplexProperty) { this.currentComplexType.getComplexProperties().add( (ComplexProperty) property); } else { this.currentComplexType.getProperties().add(property); } } // Check the declaration of a property mapping. discoverMapping(this.currentEntityType, property, currentMetadata, attrs); } else if ("navigationProperty".equalsIgnoreCase(localName)) { pushState(State.NAVIGATION_PROPERTY); NavigationProperty property = new NavigationProperty( attrs.getValue("Name")); property.setFromRole(new AssociationEnd(attrs.getValue("FromRole"))); property.setRelationship(new Association(attrs .getValue("Relationship"))); property.setToRole(new AssociationEnd(attrs.getValue("ToRole"))); currentEntityType.getAssociations().add(property); } else if ("complexType".equalsIgnoreCase(localName)) { pushState(State.COMPLEX_TYPE); currentComplexType = new ComplexType(attrs.getValue("Name")); currentComplexType.setSchema(this.currentSchema); String value = attrs.getValue("BaseType"); if (value != null) { currentComplexType.setBaseType(new ComplexType(value)); } this.currentSchema.getComplexTypes().add(currentComplexType); // register the new type. registeredComplexTypes.put(currentSchema.getNamespace().getName() + "." + currentComplexType.getName(), currentComplexType); } else if ("association".equalsIgnoreCase(localName)) { pushState(State.ASSOCIATION); currentAssociation = new Association(attrs.getValue("Name")); currentSchema.getAssociations().add(currentAssociation); registeredAssociations.put(currentSchema.getNamespace().getName() + "." + currentAssociation.getName(), currentAssociation); } else if ("end".equalsIgnoreCase(localName)) { if (getState() == State.ASSOCIATION) { pushState(State.ASSOCIATION_END); AssociationEnd end = new AssociationEnd(attrs.getValue("Role")); end.setMultiplicity(attrs.getValue("Multiplicity")); end.setType(new EntityType(attrs.getValue("Type"))); currentAssociation.getEnds().add(end); } else { pushState(State.ASSOCIATION_SET_END); AssociationSetEnd end = new AssociationSetEnd( attrs.getValue("Role")); end.setType(new EntitySet(attrs.getValue("EntitySet"))); currentAssociationSet.getEnds().add(end); } } else if ("onDelete".equalsIgnoreCase(localName)) { pushState(State.ON_DELETE); } else if ("referentialConstraint".equalsIgnoreCase(localName)) { pushState(State.REFERENTIAL_CONSTRAINT); } else if ("functionImport".equalsIgnoreCase(localName)) { currentFunctionImport = new FunctionImport(attrs.getValue("Name")); currentFunctionImport.setReturnType(attrs.getValue("ReturnType")); currentFunctionImport.setEntitySet(new EntitySet(attrs .getValue("EntitySet"))); currentFunctionImport.setMethodAccess(attrs .getValue("MethodAccess")); currentFunctionImport.setMetadata(currentMetadata); String str = attrs.getValue( Service.WCF_DATASERVICES_METADATA_NAMESPACE, "HttpMethod"); if (str != null) { currentFunctionImport.setMethod(Method.valueOf(str)); } if (State.ENTITY_CONTAINER == getState()) { currentEntityContainer.getFunctionImports().add( currentFunctionImport); } pushState(State.FUNCTION_IMPORT); } else if ("parameter".equalsIgnoreCase(localName)) { if (State.FUNCTION_IMPORT == getState()) { Parameter parameter = new Parameter(attrs.getValue("Name")); parameter.setType(attrs.getValue("Type")); parameter.setMode(attrs.getValue("Mode")); String str = attrs.getValue("MaxLength"); if (str != null) { parameter.setMaxLength(Integer.parseInt(str)); } str = attrs.getValue("Precision"); if (str != null) { parameter.setPrecision(Integer.parseInt(str)); } str = attrs.getValue("Scale"); if (str != null) { parameter.setScale(Integer.parseInt(str)); } currentFunctionImport.getParameters().add(parameter); } pushState(State.PARAMETER); } else if ("function".equalsIgnoreCase(localName)) { pushState(State.FUNCTION); } else if ("entityContainer".equalsIgnoreCase(localName)) { pushState(State.ENTITY_CONTAINER); currentEntityContainer = new EntityContainer(attrs.getValue("Name")); currentEntityContainer.setDefaultEntityContainer(Boolean .parseBoolean(attrs.getValue( Service.WCF_DATASERVICES_METADATA_NAMESPACE, "IsDefaultEntityContainer"))); String value = attrs.getValue("Extends"); if (value != null) { currentEntityContainer.setExtended(new EntityContainer(value)); } currentEntityContainer.setSchema(currentSchema); currentMetadata.getContainers().add(currentEntityContainer); registeredContainers.put(currentSchema.getNamespace().getName() + "." + currentEntityContainer.getName(), currentEntityContainer); } else if ("entitySet".equalsIgnoreCase(localName)) { pushState(State.ENTITY_SET); EntitySet entitySet = new EntitySet(attrs.getValue("Name")); registeredEntitySets.put(currentSchema.getNamespace().getName() + "." + entitySet.getName(), entitySet); entitySet.setType(new EntityType(attrs.getValue("EntityType"))); currentEntityContainer.getEntities().add(entitySet); } else if ("associationSet".equalsIgnoreCase(localName)) { pushState(State.ASSOCIATION_SET); currentAssociationSet = new AssociationSet(attrs.getValue("Name")); currentAssociationSet.setAssociation(new Association(attrs .getValue("Association"))); currentEntityContainer.getAssociations().add(currentAssociationSet); } } }