/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.olingo.server.core;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import org.apache.olingo.commons.api.edm.EdmException;
import org.apache.olingo.commons.api.edm.FullQualifiedName;
import org.apache.olingo.commons.api.edm.geo.SRID;
import org.apache.olingo.commons.api.edm.provider.CsdlAction;
import org.apache.olingo.commons.api.edm.provider.CsdlActionImport;
import org.apache.olingo.commons.api.edm.provider.CsdlAnnotatable;
import org.apache.olingo.commons.api.edm.provider.CsdlAnnotation;
import org.apache.olingo.commons.api.edm.provider.CsdlAnnotations;
import org.apache.olingo.commons.api.edm.provider.CsdlBindingTarget;
import org.apache.olingo.commons.api.edm.provider.CsdlComplexType;
import org.apache.olingo.commons.api.edm.provider.CsdlEntityContainer;
import org.apache.olingo.commons.api.edm.provider.CsdlEntitySet;
import org.apache.olingo.commons.api.edm.provider.CsdlEntityType;
import org.apache.olingo.commons.api.edm.provider.CsdlEnumMember;
import org.apache.olingo.commons.api.edm.provider.CsdlEnumType;
import org.apache.olingo.commons.api.edm.provider.CsdlFunction;
import org.apache.olingo.commons.api.edm.provider.CsdlFunctionImport;
import org.apache.olingo.commons.api.edm.provider.CsdlNavigationProperty;
import org.apache.olingo.commons.api.edm.provider.CsdlNavigationPropertyBinding;
import org.apache.olingo.commons.api.edm.provider.CsdlOnDelete;
import org.apache.olingo.commons.api.edm.provider.CsdlOnDeleteAction;
import org.apache.olingo.commons.api.edm.provider.CsdlOperation;
import org.apache.olingo.commons.api.edm.provider.CsdlParameter;
import org.apache.olingo.commons.api.edm.provider.CsdlProperty;
import org.apache.olingo.commons.api.edm.provider.CsdlPropertyRef;
import org.apache.olingo.commons.api.edm.provider.CsdlReferentialConstraint;
import org.apache.olingo.commons.api.edm.provider.CsdlReturnType;
import org.apache.olingo.commons.api.edm.provider.CsdlSchema;
import org.apache.olingo.commons.api.edm.provider.CsdlSingleton;
import org.apache.olingo.commons.api.edm.provider.CsdlTerm;
import org.apache.olingo.commons.api.edm.provider.CsdlTypeDefinition;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlAnnotationPath;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlApply;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlCast;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlCollection;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlConstantExpression;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlConstantExpression.ConstantExpressionType;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlExpression;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlIf;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlIsOf;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlLabeledElement;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlLabeledElementReference;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlNavigationPropertyPath;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlNull;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlPath;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlPropertyPath;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlPropertyValue;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlRecord;
import org.apache.olingo.commons.api.edm.provider.annotation.CsdlUrlRef;
import org.apache.olingo.commons.api.edmx.EdmxReference;
import org.apache.olingo.commons.api.edmx.EdmxReferenceInclude;
import org.apache.olingo.commons.api.edmx.EdmxReferenceIncludeAnnotation;
import org.apache.olingo.server.api.ServiceMetadata;

/**
 * This class can convert a CSDL document into EDMProvider object
 */
public class MetadataParser {
  private boolean parseAnnotations = false;
  private static final String XML_LINK_NS = "http://www.w3.org/1999/xlink";
  private ReferenceResolver referenceResolver = new DefaultReferenceResolver();
  private boolean useLocalCoreVocabularies = true;
  private boolean implicitlyLoadCoreVocabularies = false;
  private boolean recursivelyLoadReferences = false;
  private Map<String, SchemaBasedEdmProvider> globalReferenceMap = new HashMap<>();
  
  /**
   * Avoid reading the annotations in the $metadata 
   * @param parse
   * @return
   */
  public MetadataParser parseAnnotations(boolean parse) {
    this.parseAnnotations = parse;
    return this;
  }

  /**
   * Externalize the reference loading, such that they can be loaded from local caches
   * @param resolver
   * @return
   */
  public MetadataParser referenceResolver(ReferenceResolver resolver) {
    this.referenceResolver = resolver;
    return this;
  }
  
  /**
   * Load the core libraries from local classpath
   * @param load true for yes; false otherwise
   * @return
   */
  public MetadataParser useLocalCoreVocabularies(boolean load) {
    this.useLocalCoreVocabularies = load;
    return this;
  }
  
  /**
   * Load the core libraries from local classpath
   * @param load true for yes; false otherwise
   * @return
   */
  public MetadataParser recursivelyLoadReferences(boolean load) {
    this.recursivelyLoadReferences = load;
    return this;
  }  
  
  /**
   * Load the core vocabularies, irrespective of if they are defined in the $metadata
   * @param load
   * @return
   */
  public MetadataParser implicitlyLoadCoreVocabularies(boolean load) {
    this.implicitlyLoadCoreVocabularies = load;
    return this;
  }
  
  public ServiceMetadata buildServiceMetadata(Reader csdl) throws XMLStreamException {
    SchemaBasedEdmProvider provider = buildEdmProvider(csdl, this.referenceResolver,
            this.implicitlyLoadCoreVocabularies, this.useLocalCoreVocabularies, true, null);
    return new ServiceMetadataImpl(provider, provider.getReferences(), null);
  }

  public SchemaBasedEdmProvider buildEdmProvider(Reader csdl) throws XMLStreamException {
    XMLInputFactory xmlInputFactory = createXmlInputFactory();
    XMLEventReader reader = xmlInputFactory.createXMLEventReader(csdl);    
    return buildEdmProvider(reader, this.referenceResolver, this.implicitlyLoadCoreVocabularies,
            this.useLocalCoreVocabularies, true, null);
  }
  
  public SchemaBasedEdmProvider addToEdmProvider(SchemaBasedEdmProvider existing, Reader csdl)
      throws XMLStreamException {
    XMLInputFactory xmlInputFactory = createXmlInputFactory();
    XMLEventReader reader = xmlInputFactory.createXMLEventReader(csdl);
    return addToEdmProvider(existing, reader, this.referenceResolver, this.implicitlyLoadCoreVocabularies,
        this.useLocalCoreVocabularies, true, null);
  }

  protected SchemaBasedEdmProvider buildEdmProvider(Reader csdl, ReferenceResolver resolver,
                                                    boolean loadCore, boolean useLocal,
                                                    boolean loadReferenceSchemas, String namespace)
          throws XMLStreamException {
    XMLInputFactory xmlInputFactory = createXmlInputFactory();
    XMLEventReader reader = xmlInputFactory.createXMLEventReader(csdl);
    return buildEdmProvider(reader, resolver, loadCore, useLocal, loadReferenceSchemas, namespace);
  }

  protected SchemaBasedEdmProvider buildEdmProvider(InputStream csdl, ReferenceResolver resolver,
                                                    boolean loadCore, boolean useLocal,
                                                    boolean loadReferenceSchemas, String namespace)
          throws XMLStreamException {
    XMLInputFactory xmlInputFactory = createXmlInputFactory();
    XMLEventReader reader = xmlInputFactory.createXMLEventReader(csdl);
    return buildEdmProvider(reader, resolver, loadCore, useLocal, loadReferenceSchemas, namespace);
  } 

  protected SchemaBasedEdmProvider buildEdmProvider(XMLEventReader reader, ReferenceResolver resolver, boolean loadCore,
      boolean useLocal, boolean loadReferenceSchemas, String namespace) throws XMLStreamException {
    SchemaBasedEdmProvider provider = new SchemaBasedEdmProvider();
    return addToEdmProvider(provider, reader, resolver, loadCore, useLocal, loadReferenceSchemas, namespace);
  }
  
  protected SchemaBasedEdmProvider addToEdmProvider(SchemaBasedEdmProvider provider, XMLEventReader reader,
      ReferenceResolver resolver, boolean loadCore, boolean useLocal, boolean loadReferenceSchemas, String namespace)
      throws XMLStreamException {
    
    final StringBuilder xmlBase = new StringBuilder();
    
    new ElementReader<SchemaBasedEdmProvider>() {
      @Override
      void build(XMLEventReader reader, StartElement element, SchemaBasedEdmProvider provider,
          String name) throws XMLStreamException {
        if (attrNS(element, XML_LINK_NS, "base") != null) {
          xmlBase.append(attrNS(element, XML_LINK_NS, "base"));
        }
        String version = attr(element, "Version");
        if ("4.0".equals(version)) {
          readDataServicesAndReference(reader, element, provider);
        } else {
          throw new XMLStreamException("Currently only V4 is supported.");
        }
      }
    }.read(reader, null, provider, "Edmx");
    
    // make sure there is nothing left to read, due to parser error
    if(reader.hasNext()) {
      XMLEvent event = reader.peek();
      throw new XMLStreamException(
          "Failed to read complete metadata file. Failed at "
              + (event.isStartElement() ? 
                  event.asStartElement().getName().getLocalPart() : 
                  event.asEndElement().getName().getLocalPart()));
    }
    
    //load core vocabularies even though they are not defined in the references
    if (loadCore) {
      loadCoreVocabulary(provider, "Org.OData.Core.V1");
      loadCoreVocabulary(provider, "Org.OData.Capabilities.V1");
      loadCoreVocabulary(provider, "Org.OData.Measures.V1");
    }

    if (namespace != null && !namespace.equals("") && !globalReferenceMap.containsKey(namespace)) {
      globalReferenceMap.put(namespace, provider);
    }

    // load all the reference schemas
    if (resolver != null && loadReferenceSchemas) {
      loadReferencesSchemas(provider, xmlBase.length() == 0 ? null
          : fixXmlBase(xmlBase.toString()), resolver, loadCore, useLocal);
    }
    return provider;
  }

  private XMLInputFactory createXmlInputFactory() {
    XMLInputFactory factory = XMLInputFactory.newInstance();
    factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
    factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
    return factory;
  }

  private void loadReferencesSchemas(SchemaBasedEdmProvider provider,
      String xmlBase, ReferenceResolver resolver, boolean loadCore,
      boolean useLocal) {    

    for (EdmxReference reference:provider.getReferences()) {
      try {
        SchemaBasedEdmProvider refProvider = null;

        for (EdmxReferenceInclude include : reference.getIncludes()) {

          // check if the schema is already loaded before in current provider.
          if (provider.getSchemaDirectly(include.getNamespace()) != null) {
            continue;
          }
          
          if (isCoreVocabulary(include.getNamespace()) && useLocal) {
            loadCoreVocabulary(provider, include.getNamespace());
            continue;
          }

          // check if the schema is already loaded before in parent providers
          refProvider = this.globalReferenceMap.get(include.getNamespace());

          if (refProvider == null) {
            InputStream is = this.referenceResolver.resolveReference(reference.getUri(), xmlBase);
            if (is == null) {
              throw new EdmException("Failed to load Reference "+reference.getUri()+" loading failed");
            } else {
              // do not implicitly load core vocabularies any more. But if the
              // references loading the core vocabularies try to use local if we can
              refProvider = buildEdmProvider(is, resolver, false, useLocal,
                      this.recursivelyLoadReferences, include.getNamespace());
            }
          }
          
          if (refProvider != null) {
            CsdlSchema refSchema = refProvider.getSchema(include.getNamespace(), false);
            provider.addReferenceSchema(include.getNamespace(), refProvider);
            if (include.getAlias() != null) {
              refSchema.setAlias(include.getAlias());
              provider.addReferenceSchema(include.getAlias(), refProvider);
            }
          }
        }
      } catch (XMLStreamException e) {
        throw new EdmException("Failed to load Reference "+reference.getUri()+" parsing failed");
      }
    }
  }
  
  public void loadCoreVocabulary(SchemaBasedEdmProvider provider,
      String namespace) throws XMLStreamException {
    if("Org.OData.Core.V1".equalsIgnoreCase(namespace)) {
      loadLocalVocabularySchema(provider, "Org.OData.Core.V1", "Org.OData.Core.V1.xml");
    } else if ("Org.OData.Capabilities.V1".equalsIgnoreCase(namespace)) {
      loadLocalVocabularySchema(provider, "Org.OData.Capabilities.V1", "Org.OData.Capabilities.V1.xml");
    } else if ("Org.OData.Measures.V1".equalsIgnoreCase(namespace)) {
      loadLocalVocabularySchema(provider, "Org.OData.Measures.V1", "Org.OData.Measures.V1.xml");
    } else {
    	throw new XMLStreamException("Unknown namespace to load vocabulary");
    }
  }

  private boolean isCoreVocabulary(String namespace) {
    if("Org.OData.Core.V1".equalsIgnoreCase(namespace) || 
        "Org.OData.Capabilities.V1".equalsIgnoreCase(namespace) || 
        "Org.OData.Measures.V1".equalsIgnoreCase(namespace)) {
      return true;
    }
    return false;
  }

  private String fixXmlBase(String base) {
    if (base.endsWith("/")) {
      return base;
    } 
    return base+"/";
  }  
  
  private void loadLocalVocabularySchema(SchemaBasedEdmProvider provider, String namespace,
      String resource) throws XMLStreamException {
    CsdlSchema schema = provider.getVocabularySchema(namespace);
    if (schema == null) {
      InputStream is = this.getClass().getClassLoader().getResourceAsStream(resource);
      if (is != null) {
        SchemaBasedEdmProvider childProvider = buildEdmProvider(is, null, false, false, true, "");
        provider.addVocabularySchema(namespace, childProvider);
      } else {
        throw new XMLStreamException("failed to load "+resource+" core vocabulary");
      }
    }
  }  
  
  private void readDataServicesAndReference(XMLEventReader reader,
      StartElement element, SchemaBasedEdmProvider provider)
      throws XMLStreamException {
    new ElementReader<SchemaBasedEdmProvider>() {
      @Override
      void build(XMLEventReader reader, StartElement element, SchemaBasedEdmProvider provider,
          String name) throws XMLStreamException {
        if ("DataServices".equals(name)) {
          readSchema(reader, element, provider);
        } else if ("Reference".equals(name)) {
          readReference(reader, element, provider, "Reference");
        }
      }
    }.read(reader, element, provider, "DataServices", "Reference");
  }

  private void readReference(XMLEventReader reader, StartElement element,
      final SchemaBasedEdmProvider provider, String name) throws XMLStreamException {
    EdmxReference reference;
    try {
      String uri = attr(element, "Uri");
      reference = new EdmxReference(new URI(uri));
    } catch (URISyntaxException e) {
      throw new XMLStreamException(e);
    }
    new ElementReader<EdmxReference>() {
      @Override
      void build(XMLEventReader reader, StartElement element,
          EdmxReference reference, String name) throws XMLStreamException {
        if ("Include".equals(name)) {
          EdmxReferenceInclude include = new EdmxReferenceInclude(attr(element, "Namespace"),
              attr(element, "Alias"));
          reference.addInclude(include);
        } else if ("IncludeAnnotations".equals(name)) {
          EdmxReferenceIncludeAnnotation annotation = new EdmxReferenceIncludeAnnotation(
              attr(element, "TermNamespace"));
          annotation.setTargetNamespace(attr(element, "TargetNamespace"));
          annotation.setQualifier(attr(element, "Qualifier"));
          reference.addIncludeAnnotation(annotation);
        } else if ("Annotation".equals(name)) {
          readAnnotations(reader, element, reference);
        }
      }
    }.read(reader, element, reference, "Include", "IncludeAnnotations", "Annotation");
    provider.addReference(reference);
  }
  
  private void readSchema(XMLEventReader reader, StartElement element,
      SchemaBasedEdmProvider provider) throws XMLStreamException {

    new ElementReader<SchemaBasedEdmProvider>() {
      @Override
      void build(XMLEventReader reader, StartElement element, SchemaBasedEdmProvider provider, String name)
          throws XMLStreamException {
        CsdlSchema schema = new CsdlSchema();
        schema.setComplexTypes(new ArrayList<CsdlComplexType>());
        schema.setActions(new ArrayList<CsdlAction>());
        schema.setEntityTypes(new ArrayList<CsdlEntityType>());
        schema.setEnumTypes(new ArrayList<CsdlEnumType>());
        schema.setFunctions(new ArrayList<CsdlFunction>());
        schema.setTerms(new ArrayList<CsdlTerm>());
        schema.setTypeDefinitions(new ArrayList<CsdlTypeDefinition>());        
        schema.setNamespace(attr(element, "Namespace"));
        schema.setAlias(attr(element, "Alias"));
        readSchemaContents(reader, schema);
        provider.addSchema(schema);
      }
    }.read(reader, element, provider, "Schema");
  }

  private void readSchemaContents(XMLEventReader reader, CsdlSchema schema) throws XMLStreamException {
    new ElementReader<CsdlSchema>() {
      @Override
      void build(XMLEventReader reader, StartElement element, CsdlSchema schema, String name)
          throws XMLStreamException {
        if ("Action".equals(name)) {
          readAction(reader, element, schema);
        } else if ("Annotations".equals(name)) {
          readAnnotationGroup(reader, element, schema);
        } else if ("Annotation".equals(name)) {
          readAnnotations(reader, element, schema);
        } else if ("ComplexType".equals(name)) {
          readComplexType(reader, element, schema);
        } else if ("EntityContainer".equals(name)) {
          readEntityContainer(reader, element, schema);
        } else if ("EntityType".equals(name)) {
          readEntityType(reader, element, schema);
        } else if ("EnumType".equals(name)) {
          readEnumType(reader, element, schema);
        } else if ("Function".equals(name)) {
          readFunction(reader, element, schema);
        } else if ("Term".equals(name)) {
          schema.getTerms().add(readTerm(reader, element));
        } else if ("TypeDefinition".equals(name)) {
          schema.getTypeDefinitions().add(readTypeDefinition(reader, element));
        }
      }
    }.read(reader, null, schema, "Action", "Annotations", "Annotation", "ComplexType",
        "EntityContainer", "EntityType", "EnumType", "Function", "Term", "TypeDefinition");
  }

  private void readAction(XMLEventReader reader, StartElement element, CsdlSchema schema)
      throws XMLStreamException {

    CsdlAction action = new CsdlAction();
    action.setParameters(new ArrayList<CsdlParameter>());
    action.setName(attr(element, "Name"));
    action.setBound(Boolean.parseBoolean(attr(element, "IsBound")));
    String entitySetPath = attr(element, "EntitySetPath");
    if (entitySetPath != null) {
      // TODO: need to parse into binding and path.
      action.setEntitySetPath(entitySetPath);
    }
    readOperationParameters(reader, action);
    schema.getActions().add(action);
  }

  private FullQualifiedName readType(StartElement element) {
    String type = attr(element, "Type");
    if (type != null && type.startsWith("Collection(") && type.endsWith(")")) {
      return new FullQualifiedName(type.substring(11, type.length() - 1));
    }
    return new FullQualifiedName(type);
  }

  private boolean isCollectionType(StartElement element) {
    String type = attr(element, "Type");
    if (type != null && type.startsWith("Collection(") && type.endsWith(")")) {
      return true;
    }
    return false;
  }

  private void readReturnType(XMLEventReader reader, StartElement element,
      CsdlOperation operation) throws XMLStreamException {
    CsdlReturnType returnType = new CsdlReturnType();
    returnType.setType(readType(element));
    returnType.setCollection(isCollectionType(element));
    returnType.setNullable(Boolean.parseBoolean(attr(element, "Nullable")));

    String maxLength = attr(element, "MaxLength");
    if (maxLength != null) {
      returnType.setMaxLength(Integer.parseInt(maxLength));
    }
    String precision = attr(element, "Precision");
    if (precision != null) {
      returnType.setPrecision(Integer.parseInt(precision));
    }
    String scale = attr(element, "Scale");
    if (scale != null) {
      returnType.setScale(Integer.parseInt(scale));
    }
    String srid = attr(element, "SRID");
    if (srid != null) {
      returnType.setSrid(SRID.valueOf(srid));
    }
    peekAnnotations(reader, element.getName().getLocalPart(), returnType);
    operation.setReturnType(returnType);
  }

  private void readParameter(XMLEventReader reader, StartElement element,
      CsdlOperation operation) throws XMLStreamException {
    CsdlParameter parameter = new CsdlParameter();
    parameter.setName(attr(element, "Name"));
    parameter.setType(readType(element));
    parameter.setCollection(isCollectionType(element));
    parameter.setNullable(Boolean.parseBoolean(attr(element, "Nullable")));

    String maxLength = attr(element, "MaxLength");
    if (maxLength != null) {
      parameter.setMaxLength(Integer.parseInt(maxLength));
    }
    String precision = attr(element, "Precision");
    if (precision != null) {
      parameter.setPrecision(Integer.parseInt(precision));
    }
    String scale = attr(element, "Scale");
    if (scale != null) {
      parameter.setScale(Integer.parseInt(scale));
    }
    String srid = attr(element, "SRID");
    if (srid != null) {
      parameter.setSrid(SRID.valueOf(srid));
    }
    peekAnnotations(reader, element.getName().getLocalPart(), parameter);
    operation.getParameters().add(parameter);
  }

  private CsdlTypeDefinition readTypeDefinition(XMLEventReader reader,
      StartElement element) throws XMLStreamException {
    CsdlTypeDefinition td = new CsdlTypeDefinition();
    td.setName(attr(element, "Name"));
    td.setUnderlyingType(new FullQualifiedName(attr(element, "UnderlyingType")));
    if (attr(element, "Unicode") != null) {
      td.setUnicode(Boolean.parseBoolean(attr(element, "Unicode")));
    }

    String maxLength = attr(element, "MaxLength");
    if (maxLength != null) {
      td.setMaxLength(Integer.parseInt(maxLength));
    }
    String precision = attr(element, "Precision");
    if (precision != null) {
      td.setPrecision(Integer.parseInt(precision));
    }
    String scale = attr(element, "Scale");
    if (scale != null) {
      td.setScale(Integer.parseInt(scale));
    }
    String srid = attr(element, "SRID");
    if (srid != null) {
      td.setSrid(SRID.valueOf(srid));
    }
    peekAnnotations(reader, element.getName().getLocalPart(), td);
    return td;
  }

  private CsdlTerm readTerm(XMLEventReader reader, StartElement element) throws XMLStreamException {
    CsdlTerm term = new CsdlTerm();
    term.setName(attr(element, "Name"));
    term.setType(attr(element, "Type"));
    
    if (attr(element, "BaseTerm") != null) {
      term.setBaseTerm(attr(element, "BaseTerm"));
    }
    if (attr(element, "DefaultValue") != null) {
      term.setDefaultValue(attr(element, "DefaultValue"));
    }
    if (attr(element, "AppliesTo") != null) {
      String[] appliesTo = attr(element, "AppliesTo").split("\\s+");
      term.setAppliesTo(Arrays.asList(appliesTo));
    }
    term.setNullable(Boolean.parseBoolean(attr(element, "Nullable")));
    String maxLength = attr(element, "MaxLength");
    if (maxLength != null) {
      term.setMaxLength(Integer.parseInt(maxLength));
    }
    String precision = attr(element, "Precision");
    if (precision != null) {
      term.setPrecision(Integer.parseInt(precision));
    }
    String scale = attr(element, "Scale");
    if (scale != null) {
      term.setScale(Integer.parseInt(scale));
    }
    String srid = attr(element, "SRID");
    if (srid != null) {
      term.setSrid(SRID.valueOf(srid));
    }
    peekAnnotations(reader, "Term", term);
    return term;
  }

  private void readAnnotationGroup(XMLEventReader reader, StartElement element,
      CsdlSchema schema) throws XMLStreamException {
    final CsdlAnnotations annotations = new CsdlAnnotations();
    annotations.setTarget(attr(element, "Target"));
    annotations.setQualifier(attr(element, "Qualifier"));
    peekAnnotations(reader, element.getName().getLocalPart(), annotations);
    schema.getAnnotationGroups().add(annotations);
  }

  private void peekAnnotations(XMLEventReader reader, String endName,
      CsdlAnnotatable edmObject) throws XMLStreamException {
    if(!parseAnnotations) {
      return;
    }
    while (reader.hasNext()) {
      XMLEvent event = reader.peek();

      if (!event.isStartElement() && !event.isEndElement()) {
        reader.nextEvent();
        continue;
      }
      
      if (event.isStartElement()) {
        StartElement element = event.asStartElement();
        if ("Annotation".equals(element.getName().getLocalPart())) {
          reader.nextEvent();
          readAnnotations(reader, element, edmObject);
        }
      }
      
      if (event.isEndElement()) {
        EndElement element = event.asEndElement();
        if ("Annotation".equals(element.getName().getLocalPart())) {
          reader.nextEvent();
        }
        
        if (element.getName().getLocalPart().equals(endName)) {
          return;
        }
      }
    }
  }
  
  private void readAnnotations(XMLEventReader reader, StartElement element,
      CsdlAnnotatable edmObject) throws XMLStreamException {
    if (!parseAnnotations) {
      return;
    }
    final CsdlAnnotation annotation = new CsdlAnnotation();
    annotation.setTerm(attr(element, "Term"));
    for (ConstantExpressionType type:ConstantExpressionType.values()) {
      if (attr(element, type.name()) != null) {
        annotation.setExpression(new CsdlConstantExpression(
            type, attr(element, type.name())));
      }        
    }
    readExpressions(reader, element, annotation);
    edmObject.getAnnotations().add(annotation);
  } 

  private <T> void write(T t, CsdlExpression expr) throws XMLStreamException {
    if(t instanceof CsdlAnnotation) {
      ((CsdlAnnotation)t).setExpression(expr);
    } else if (t instanceof CsdlUrlRef) {
      ((CsdlUrlRef)t).setValue(expr);
    } else if (t instanceof CsdlCast) {
      ((CsdlCast)t).setValue(expr);
    } else if (t instanceof CsdlLabeledElement) {
      ((CsdlLabeledElement)t).setValue(expr);
    } else if (t instanceof CsdlIsOf) {
      ((CsdlIsOf)t).setValue(expr);
    } else if (t instanceof CsdlCollection) {
      ((CsdlCollection)t).getItems().add(((CsdlCollection)t).getItems().size(), expr);
    } else if (t instanceof CsdlApply) {
      ((CsdlApply)t).getParameters().add(expr);
    } else if (t instanceof CsdlIf) {
      if (((CsdlIf)t).getGuard() == null) {
        ((CsdlIf)t).setGuard(expr);
      } else if (((CsdlIf)t).getThen() == null) {
        ((CsdlIf)t).setThen(expr);
      } else {
        ((CsdlIf)t).setElse(expr);
      }
    } else if (t instanceof CsdlPropertyValue) {
      ((CsdlPropertyValue)t).setValue(expr);
    } else {
      throw new XMLStreamException("Unknown expression parent in Annoatation");
    }
  }
  
  private <T> void readExpressions(XMLEventReader reader,
      StartElement element, T target)
      throws XMLStreamException {
    new ElementReader<T>() {
      @Override
      void build(XMLEventReader reader, StartElement element, T target, String name)
          throws XMLStreamException {
        
        // element based expressions
        if (!"Annotation".equals(name)) {
          // attribute based expressions.
          readAttributeExpressions(element, target);        
          
          for (ConstantExpressionType type:ConstantExpressionType.values()) {
            if (name.equals(type.name()) && reader.peek().isCharacters()) {
              CsdlExpression expr = new CsdlConstantExpression(type, elementValue(reader, element));
              write(target, expr);
            }        
          }
        }
        
        if ("Collection".equals(name)) {
          CsdlCollection expr = new CsdlCollection();
          readExpressions(reader, element, expr);
          write(target, expr);
        } else if ("AnnotationPath".equals(name)) {
          write(target, new CsdlAnnotationPath().setValue(elementValue(reader, element)));
        } else if ("NavigationPropertyPath".equals(name)) {
          write(target, new CsdlNavigationPropertyPath()
              .setValue(elementValue(reader, element)));
        } else if ("Path".equals(name)) {
          write(target, new CsdlPath().setValue(elementValue(reader, element)));
        } else if ("PropertyPath".equals(name)) {
          write(target, new CsdlPropertyPath().setValue(elementValue(reader, element)));
        } else if ("UrlRef".equals(name)) {
          CsdlUrlRef expr = new CsdlUrlRef();
          readExpressions(reader, element, expr);
          write(target, expr);
        } else if ("Apply".equals(name)) {
          CsdlApply expr = new CsdlApply();
          expr.setFunction(attr(element, "Function"));
          readExpressions(reader, element, expr);
          write(target, expr);
        } else if ("Cast".equals(name)) {
          CsdlCast expr = new CsdlCast();
          expr.setType(attr(element, "Type"));
          readExpressions(reader, element, expr);
          write(target, expr);
        } else if ("If".equals(name)) {
          CsdlIf expr = new CsdlIf();
          readExpressions(reader, element, expr);
          write(target, expr);
        } else if ("IsOf".equals(name)) {
          CsdlIsOf expr = new CsdlIsOf();
          expr.setType(attr(element, "Type"));
          readExpressions(reader, element, expr);
          write(target, expr);
        } else if ("LabeledElement".equals(name)) {
          CsdlLabeledElement expr = new CsdlLabeledElement();
          expr.setName(attr(element, "Name"));
          readExpressions(reader, element, expr);
          write(target, expr);
        } else if ("LabeledElementReference".equals(name)) {
          CsdlLabeledElementReference expr = new CsdlLabeledElementReference();
          expr.setValue(elementValue(reader, element));
          write(target, expr);
        } else if ("Null".equals(name)) {
          write(target, new CsdlNull());
        } else if ("Record".equals(name)) {
          CsdlRecord expr = new CsdlRecord();
          expr.setType(attr(element, "Type"));          
          readPropertyValues(reader, element, expr);
          write(target, expr);          
        } else if ("Annotation".equals(name)) {
          readAnnotations(reader, element, (CsdlAnnotatable)target);
        }
      }
    }.read(reader, element, target, "Collection", "AnnotationPath",
        "NavigationPropertyPath", "Path", "PropertyPath", "UrlRef",
        "Apply", "Function", "Cast", "If", "IsOf", "LabeledElement",
        "LabeledElementReference", "Null", "Record","Binary", "Bool", "Date",
        "DateTimeOffset", "Decimal", "Duration", "EnumMember", "Float", "Guid",
        "Int", "String", "TimeOfDay", "Annotation");
  }
  
  private <T> void readAttributeExpressions(StartElement element, T target)
      throws XMLStreamException {
    // attribute based expressions
    for (ConstantExpressionType type:ConstantExpressionType.values()) {
      if (attr(element, type.name()) != null) {
        write(target, new CsdlConstantExpression(
            type, attr(element, type.name())));
      }        
    }
    
    if (attr(element,  "AnnotationPath") != null) {
     write(target, new CsdlAnnotationPath().setValue(attr(element,  "AnnotationPath"))); 
    }
    if (attr(element,  "NavigationPropertyPath") != null) {
      write(target, new CsdlNavigationPropertyPath()
          .setValue(attr(element, "NavigationPropertyPath"))); 
    }
    if (attr(element,  "Path") != null) {
      write(target, new CsdlPath().setValue(attr(element, "Path"))); 
    }
    if (attr(element,  "PropertyPath") != null) {
      write(target, new CsdlPropertyPath().setValue(attr(element, "PropertyPath"))); 
    }
    if (attr(element,  "UrlRef") != null) {
      write(target, new CsdlUrlRef().setValue(new CsdlConstantExpression(
          ConstantExpressionType.String, attr(element, "UrlRef"))));
    }
  }  
  
  private String elementValue(XMLEventReader reader, StartElement element) throws XMLStreamException {
    while (reader.hasNext()) {
      XMLEvent event = reader.peek();
      if (event.isStartElement() || event.isEndElement()) {
        return null;
      } else if (event.isCharacters()){
        reader.nextEvent();
        String data = event.asCharacters().getData();
        if (data.trim().length() > 0) {
          return data.trim();
        }
      }
    }    
    return null;
  }
  
  private void readPropertyValues(XMLEventReader reader,
      StartElement element, CsdlRecord record) throws XMLStreamException {
    
    new ElementReader<CsdlRecord>() {
      @Override
      void build(XMLEventReader reader, StartElement element, CsdlRecord record, String name)
          throws XMLStreamException {
        if ("PropertyValue".equals(name)) {
          CsdlPropertyValue value = new CsdlPropertyValue();
          value.setProperty(attr(element, "Property"));
          readAttributeExpressions(element, value);
          readExpressions(reader, element, value);
          record.getPropertyValues().add(value);
        } else if ("Annotation".equals(name)) {
          readAnnotations(reader, element, record);
        }
      }
    }.read(reader, element, record, "PropertyValue", "Annotation");    
  }  
  
  private void readFunction(XMLEventReader reader, StartElement element, CsdlSchema schema)
      throws XMLStreamException {
    CsdlFunction function = new CsdlFunction();
    function.setParameters(new ArrayList<CsdlParameter>());
    function.setName(attr(element, "Name"));
    function.setBound(Boolean.parseBoolean(attr(element, "IsBound")));
    function.setComposable(Boolean.parseBoolean(attr(element, "IsComposable")));
    String entitySetPath = attr(element, "EntitySetPath");
    if (entitySetPath != null) {
      // TODO: need to parse into binding and path.
      function.setEntitySetPath(entitySetPath);
    }
    readOperationParameters(reader, function);
    schema.getFunctions().add(function);
  }

  private void readOperationParameters(XMLEventReader reader, final CsdlOperation operation)
      throws XMLStreamException {
    new ElementReader<CsdlOperation>() {
      @Override
      void build(XMLEventReader reader, StartElement element, CsdlOperation operation, String name)
          throws XMLStreamException {
        if ("Parameter".equals(name)) {
          readParameter(reader, element, operation);
        } else if ("ReturnType".equals(name)) {
          readReturnType(reader, element, operation);
        } else if ("Annotation".equals(name)) {
          readAnnotations(reader, element, operation);
        }
      }
    }.read(reader, null, operation, "Parameter", "ReturnType", "Annotation");
  }

  private void readEnumType(XMLEventReader reader, StartElement element, CsdlSchema schema)
      throws XMLStreamException {
    CsdlEnumType type = new CsdlEnumType();
    type.setMembers(new ArrayList<CsdlEnumMember>());
    type.setName(attr(element, "Name"));
    if (attr(element, "UnderlyingType") != null) {
      type.setUnderlyingType(new FullQualifiedName(attr(element, "UnderlyingType")));
    }
    type.setFlags(Boolean.parseBoolean(attr(element, "IsFlags")));

    readEnumMembers(reader, element, type);
    schema.getEnumTypes().add(type);
  }

  private void readEnumMembers(XMLEventReader reader, StartElement element, CsdlEnumType type)
      throws XMLStreamException {
    
    new ElementReader<CsdlEnumType>() {
      @Override
      void build(XMLEventReader reader, StartElement element, CsdlEnumType type, String name)
          throws XMLStreamException {
        if ("Member".equals(name)) {
          CsdlEnumMember member = new CsdlEnumMember();
          member.setName(attr(element, "Name"));
          member.setValue(attr(element, "Value"));
          peekAnnotations(reader, name, member);
          type.getMembers().add(member);
        } else if ("Annotation".equals(name)) {
          readAnnotations(reader, element, type);
        }
      }
    }.read(reader, element, type, "Member", "Annotation");
  }

  private void readEntityType(XMLEventReader reader, StartElement element, CsdlSchema schema)
      throws XMLStreamException {
    CsdlEntityType entityType = new CsdlEntityType();
    entityType.setProperties(new ArrayList<CsdlProperty>());
    entityType.setNavigationProperties(new ArrayList<CsdlNavigationProperty>());
    entityType.setKey(new ArrayList<CsdlPropertyRef>());
    entityType.setName(attr(element, "Name"));
    if (attr(element, "BaseType") != null) {
      entityType.setBaseType(new FullQualifiedName(attr(element, "BaseType")));
    }
    entityType.setAbstract(Boolean.parseBoolean(attr(element, "Abstract")));
    entityType.setOpenType(Boolean.parseBoolean(attr(element, "OpenType")));
    entityType.setHasStream(Boolean.parseBoolean(attr(element, "HasStream")));
    readEntityProperties(reader, entityType);
    schema.getEntityTypes().add(entityType);
  }

  private void readEntityProperties(XMLEventReader reader, CsdlEntityType entityType)
      throws XMLStreamException {
    new ElementReader<CsdlEntityType>() {
      @Override
      void build(XMLEventReader reader, StartElement element, CsdlEntityType entityType, String name)
          throws XMLStreamException {
        if ("Property".equals(name)) {
          entityType.getProperties().add(readProperty(reader, element));
        } else if ("NavigationProperty".equals(name)) {
          entityType.getNavigationProperties().add(readNavigationProperty(reader, element));
        } else if ("Key".equals(name)) {
          readKey(reader, element, entityType);
        } else if ("Annotation".equals(name)) {
          readAnnotations(reader, element, entityType);
        }
      }
    }.read(reader, null, entityType, "Property", "NavigationProperty", "Key", "Annotation");
  }

  private void readKey(XMLEventReader reader, StartElement element, CsdlEntityType entityType)
      throws XMLStreamException {
    new ElementReader<CsdlEntityType>() {
      @Override
      void build(XMLEventReader reader, StartElement element, CsdlEntityType entityType, String name)
          throws XMLStreamException {
        CsdlPropertyRef ref = new CsdlPropertyRef();
        ref.setName(attr(element, "Name"));
        ref.setAlias(attr(element, "Alias"));
        entityType.getKey().add(ref);
      }
    }.read(reader, element, entityType, "PropertyRef");
  }

  private CsdlNavigationProperty readNavigationProperty(XMLEventReader reader, StartElement element)
      throws XMLStreamException {
    CsdlNavigationProperty property = new CsdlNavigationProperty();
    property.setReferentialConstraints(new ArrayList<CsdlReferentialConstraint>());

    property.setName(attr(element, "Name"));
    property.setType(readType(element));
    property.setCollection(isCollectionType(element));
    property.setNullable(Boolean.parseBoolean(attr(element, "Nullable") == null ? "true" : attr(element, "Nullable")));
    property.setPartner(attr(element, "Partner"));
    property.setContainsTarget(Boolean.parseBoolean(attr(element, "ContainsTarget")));

    new ElementReader<CsdlNavigationProperty>() {
      @Override
      void build(XMLEventReader reader, StartElement element, CsdlNavigationProperty property,
          String name) throws XMLStreamException {
        if ("ReferentialConstraint".equals(name)) {
          CsdlReferentialConstraint constraint = new CsdlReferentialConstraint();
          constraint.setProperty(attr(element, "Property"));
          constraint.setReferencedProperty(attr(element, "ReferencedProperty"));
          peekAnnotations(reader, name, constraint);
          property.getReferentialConstraints().add(constraint);
        } else if ("OnDelete".equals(name)) {
          CsdlOnDelete delete = new CsdlOnDelete();
          delete.setAction(CsdlOnDeleteAction.valueOf(attr(element, "Action")));
          property.setOnDelete(delete);
          peekAnnotations(reader, name, delete);
        } else if ("Annotation".equals(name)) {
          readAnnotations(reader, element, property);
        }
      }
    }.read(reader, element, property, "ReferentialConstraint", "OnDelete", "Annotation");
    return property;
  }

  private static String attr(StartElement element, String name) {
    Attribute attr = element.getAttributeByName(new QName(name));
    if (attr != null) {
      return attr.getValue();
    }
    return null;
  }

  private static String attrNS(StartElement element, String ns, String name) {
    Attribute attr = element.getAttributeByName(new QName(ns, name));
    if (attr != null) {
      return attr.getValue();
    }
    return null;
  }  
  
  private CsdlProperty readProperty(XMLEventReader reader, StartElement element)
      throws XMLStreamException {
    CsdlProperty property = new CsdlProperty();
    property.setName(attr(element, "Name"));
    property.setType(readType(element));
    property.setCollection(isCollectionType(element));
    property.setNullable(Boolean.parseBoolean(attr(element, "Nullable") == null ? "true" : attr(
        element, "Nullable")));
    if (attr(element, "Unicode") != null) {
      property.setUnicode(Boolean.parseBoolean(attr(element, "Unicode")));
    }

    String maxLength = attr(element, "MaxLength");
    if (maxLength != null) {
      property.setMaxLength(Integer.parseInt(maxLength));
    }
    String precision = attr(element, "Precision");
    if (precision != null) {
      property.setPrecision(Integer.parseInt(precision));
    }
    String scale = attr(element, "Scale");
    if (scale != null) {
      property.setScale(Integer.parseInt(scale));
    }
    String srid = attr(element, "SRID");
    if (srid != null) {
      property.setSrid(SRID.valueOf(srid));
    }
    String defaultValue = attr(element, "DefaultValue");
    if (defaultValue != null) {
      property.setDefaultValue(defaultValue);
    }
    peekAnnotations(reader, element.getName().getLocalPart(), property);
    return property;
  }

  private void readEntityContainer(XMLEventReader reader, StartElement element, CsdlSchema schema)
      throws XMLStreamException {
    final CsdlEntityContainer container = new CsdlEntityContainer();
    container.setName(attr(element, "Name"));
    if (attr(element, "Extends") != null) {
      container.setExtendsContainer(attr(element, "Extends"));
    }
    container.setActionImports(new ArrayList<CsdlActionImport>());
    container.setFunctionImports(new ArrayList<CsdlFunctionImport>());
    container.setEntitySets(new ArrayList<CsdlEntitySet>());
    container.setSingletons(new ArrayList<CsdlSingleton>());

    new ElementReader<CsdlSchema>() {
      @Override
      void build(XMLEventReader reader, StartElement element, CsdlSchema schema, String name)
          throws XMLStreamException {
        if ("EntitySet".equals(name)) {
          readEntitySet(reader, element, container);
        } else if ("Singleton".equals(name)) {
          readSingleton(reader, element, container);
        } else if ("ActionImport".equals(name)) {
          readActionImport(reader, element, container);
        } else if ("FunctionImport".equals(name)) {
          readFunctionImport(reader, element, container);
        } else if ("Annotation".equals(name)) {
          readAnnotations(reader, element, container);
        }
      }

      private void readFunctionImport(XMLEventReader reader,
          StartElement element, CsdlEntityContainer container)
          throws XMLStreamException {
        CsdlFunctionImport functionImport = new CsdlFunctionImport();
        functionImport.setName(attr(element, "Name"));
        functionImport.setFunction(new FullQualifiedName(attr(element, "Function")));
        functionImport.setIncludeInServiceDocument(Boolean.parseBoolean(attr(element,
            "IncludeInServiceDocument")));

        String entitySet = attr(element, "EntitySet");
        if (entitySet != null) {
          functionImport.setEntitySet(entitySet);
        }
        peekAnnotations(reader, "FunctionImport", functionImport);
        container.getFunctionImports().add(functionImport);
      }

      private void readActionImport(XMLEventReader reader,
          StartElement element, CsdlEntityContainer container)
          throws XMLStreamException {
        CsdlActionImport actionImport = new CsdlActionImport();
        actionImport.setName(attr(element, "Name"));
        actionImport.setAction(new FullQualifiedName(attr(element, "Action")));

        String entitySet = attr(element, "EntitySet");
        if (entitySet != null) {
          actionImport.setEntitySet(entitySet);
        }
        peekAnnotations(reader, "ActionImport", actionImport);
        container.getActionImports().add(actionImport);
      }

      private void readSingleton(XMLEventReader reader, StartElement element,
          CsdlEntityContainer container) throws XMLStreamException {
        CsdlSingleton singleton = new CsdlSingleton();
        singleton.setNavigationPropertyBindings(new ArrayList<CsdlNavigationPropertyBinding>());
        singleton.setName(attr(element, "Name"));
        singleton.setType(new FullQualifiedName(attr(element, "Type")));
        singleton.setNavigationPropertyBindings(new ArrayList<CsdlNavigationPropertyBinding>());
        readNavigationPropertyBindings(reader, element, singleton);
        container.getSingletons().add(singleton);
      }

      private void readEntitySet(XMLEventReader reader, StartElement element,
          CsdlEntityContainer container) throws XMLStreamException {
        CsdlEntitySet entitySet = new CsdlEntitySet();
        entitySet.setName(attr(element, "Name"));
        entitySet.setType(new FullQualifiedName(attr(element, "EntityType")));
        entitySet.setIncludeInServiceDocument(Boolean.parseBoolean(attr(element,
            "IncludeInServiceDocument")));
        entitySet.setNavigationPropertyBindings(new ArrayList<CsdlNavigationPropertyBinding>());
        readNavigationPropertyBindings(reader, element, entitySet);
        container.getEntitySets().add(entitySet);
      }

      private void readNavigationPropertyBindings(XMLEventReader reader, StartElement element,
          CsdlBindingTarget entitySet) throws XMLStreamException {
        new ElementReader<CsdlBindingTarget>() {
          @Override
          void build(XMLEventReader reader, StartElement element,
              CsdlBindingTarget entitySet, String name) throws XMLStreamException {
            if ("NavigationPropertyBinding".equals(name)) {
              CsdlNavigationPropertyBinding binding = new CsdlNavigationPropertyBinding();
              binding.setPath(attr(element, "Path"));
              binding.setTarget(attr(element, "Target"));
              entitySet.getNavigationPropertyBindings().add(binding);
            } else if ("Annotation".equals(name)) {
              readAnnotations(reader, element, entitySet);
            }
          }

        }.read(reader, element, entitySet, "NavigationPropertyBinding", "Annotation");
      }
    }.read(reader, element, schema, "EntitySet", "Singleton", "ActionImport", "FunctionImport", "Annotation");
    schema.setEntityContainer(container);
  }

  private void readComplexType(XMLEventReader reader, StartElement element, CsdlSchema schema)
      throws XMLStreamException {
    CsdlComplexType complexType = new CsdlComplexType();
    complexType.setProperties(new ArrayList<CsdlProperty>());
    complexType.setNavigationProperties(new ArrayList<CsdlNavigationProperty>());
    complexType.setName(attr(element, "Name"));
    if (attr(element, "BaseType") != null) {
      complexType.setBaseType(new FullQualifiedName(attr(element, "BaseType")));
    }
    complexType.setAbstract(Boolean.parseBoolean(attr(element, "Abstract")));
    complexType.setOpenType(Boolean.parseBoolean(attr(element, "OpenType")));
    readProperties(reader, complexType);

    schema.getComplexTypes().add(complexType);
  }

  private void readProperties(XMLEventReader reader, CsdlComplexType complexType)
      throws XMLStreamException {
    new ElementReader<CsdlComplexType>() {
      @Override
      void build(XMLEventReader reader, StartElement element, CsdlComplexType complexType, String name)
          throws XMLStreamException {
        if ("Property".equals(name)) {
          complexType.getProperties().add(readProperty(reader, element));
        } else if ("NavigationProperty".equals(name)) {
          complexType.getNavigationProperties().add(readNavigationProperty(reader, element));
        } else if ("Annotation".equals(name)) {
          readAnnotations(reader, element, complexType);
        }
      }
    }.read(reader, null, complexType, "Property", "NavigationProperty", "Annotation");
  }

  abstract class ElementReader<T> {
    void read(XMLEventReader reader, StartElement parentElement, T t, String... names)
        throws XMLStreamException {
      while (reader.hasNext()) {
        XMLEvent event = reader.peek();

        if (!parseAnnotations) {
          XMLEvent eventBefore = event;
          event = skipAnnotations(reader, event);
          // if annotation is stripped start again
          if (eventBefore != event) {            
            continue;
          }
        }

        if (!event.isStartElement() && !event.isEndElement()) {
          reader.nextEvent();
          continue;
        }

        if (parentElement != null && event.isEndElement()
            && ((EndElement) event).getName().equals(parentElement.getName())) {
          // end reached
          break;
        }

        boolean hit = false;

        for (String name : names) {
          if (event.isStartElement()) {
            StartElement element = event.asStartElement();
            if (element.getName().getLocalPart().equals(name)) {              
              reader.nextEvent(); // advance cursor start which is current
              build(reader, element, t, name);
              hit = true;
              break;
            }
          }
          if (event.isEndElement()) {
            EndElement e = event.asEndElement();
            if (e.getName().getLocalPart().equals(name)) {
              reader.nextEvent(); // advance cursor to end which is current
              hit = true;
              break;
            }
          }
        }
        if (!hit) {
          break;
        }
      }
    }

    private XMLEvent skipAnnotations(XMLEventReader reader, XMLEvent event)
        throws XMLStreamException {
      boolean skip = false;

      while (reader.hasNext()) {
        if (event.isStartElement()) {
          StartElement element = event.asStartElement();
          if ("Annotation".equals(element.getName().getLocalPart())) {
            skip = true;
          }
        }
        if (event.isEndElement()) {
          EndElement element = event.asEndElement();
          if ("Annotation".equals(element.getName().getLocalPart())) {
            return reader.peek();
          }
        }
        if (skip) {
          event = reader.nextEvent();
        } else {
          return event;
        }
      }
      return event;
    }

    abstract void build(XMLEventReader reader, StartElement element, T t, String name)
        throws XMLStreamException;
  }
  
  private static class DefaultReferenceResolver implements ReferenceResolver {
    @Override
    public InputStream resolveReference(URI referenceUri, String xmlBase) {
      InputStream in = null;
      try {
        if (referenceUri.isAbsolute()) {
          URL schemaURL = referenceUri.toURL();
          in = schemaURL.openStream();
        } else {
          if (xmlBase != null) {
            URL schemaURL = new URL(xmlBase+referenceUri.toString());
            in = schemaURL.openStream();
          } else {
            in = this.getClass().getClassLoader().getResourceAsStream(referenceUri.getPath());
            if (in == null) {
              throw new EdmException("No xml:base set to read the references from the metadata");
            }
          }        
        }
        return in;
      } catch (MalformedURLException e) {
        throw new EdmException(e);
      } catch (IOException e) {
        throw new EdmException(e);
      }
    }
  }   
}
