/**
   * Returns the object representation for the given property element.
   *
   * @param xmlStream cursor must point at the <code>START_ELEMENT</code> event of the property,
   *     afterwards points at the next event after the <code>END_ELEMENT</code> of the property
   * @param propDecl property declaration
   * @param crs default SRS for all a descendant geometry properties
   * @param occurence
   * @return object representation for the given property element.
   * @throws XMLParsingException
   * @throws XMLStreamException
   * @throws UnknownCRSException
   */
  public Property<?> parseProperty(
      XMLStreamReaderWrapper xmlStream, PropertyType propDecl, CRS crs, int occurence)
      throws XMLParsingException, XMLStreamException, UnknownCRSException {

    Property<?> property = null;
    QName propName = xmlStream.getName();
    LOG.debug("- parsing property (begin): " + xmlStream.getCurrentEventInfo());
    LOG.debug("- property declaration: " + propDecl);

    CustomPropertyReader<?> parser = ptToParser.get(propDecl);

    if (parser == null) {
      if (propDecl instanceof SimplePropertyType) {
        property =
            createSimpleProperty(
                xmlStream, (SimplePropertyType) propDecl, xmlStream.getElementText().trim());
      } else if (propDecl instanceof GeometryPropertyType) {
        String href = xmlStream.getAttributeValue(CommonNamespaces.XLNNS, "href");
        if (href != null) {
          // TODO respect geometry type information (Point, Surface, etc.)
          GeometryReference<Geometry> refGeometry = null;
          if (specialResolver != null) {
            refGeometry =
                new GeometryReference<Geometry>(specialResolver, href, xmlStream.getSystemId());
          } else {
            refGeometry = new GeometryReference<Geometry>(idContext, href, xmlStream.getSystemId());
          }
          idContext.addReference(refGeometry);
          property = new GenericProperty<Geometry>(propDecl, propName, refGeometry);
          xmlStream.nextTag();
        } else {
          xmlStream.nextTag();
          Geometry geometry = null;
          geometry = geomReader.parse(xmlStream, crs);
          property = new GenericProperty<Geometry>(propDecl, propName, geometry);
          xmlStream.nextTag();
        }
      } else if (propDecl instanceof FeaturePropertyType) {
        String uri = xmlStream.getAttributeValue(CommonNamespaces.XLNNS, "href");
        if (uri != null) {
          FeatureReference refFeature = null;
          if (specialResolver != null) {
            refFeature = new FeatureReference(specialResolver, uri, xmlStream.getSystemId());
          } else {
            refFeature = new FeatureReference(idContext, uri, xmlStream.getSystemId());
          }
          idContext.addReference(refFeature);
          property = new GenericProperty<Feature>(propDecl, propName, refFeature);
          xmlStream.nextTag();
        } else {
          // inline feature
          if (xmlStream.nextTag() != START_ELEMENT) {
            String msg = Messages.getMessage("ERROR_INVALID_FEATURE_PROPERTY", propName);
            throw new XMLParsingException(xmlStream, msg);
          }
          // TODO make this check (no constraints on contained feature type) better
          if (((FeaturePropertyType) propDecl).getFTName() != null) {
            FeatureType expectedFt = ((FeaturePropertyType) propDecl).getValueFt();
            FeatureType presentFt = lookupFeatureType(xmlStream, xmlStream.getName());
            if (!schema.isSubType(expectedFt, presentFt)) {
              String msg =
                  Messages.getMessage(
                      "ERROR_PROPERTY_WRONG_FEATURE_TYPE",
                      expectedFt.getName(),
                      propName,
                      presentFt.getName());
              throw new XMLParsingException(xmlStream, msg);
            }
          }
          Feature subFeature = parseFeature(xmlStream, crs);
          property = new GenericProperty<Feature>(propDecl, propName, subFeature);
          xmlStream.skipElement();
        }
      } else if (propDecl instanceof CustomPropertyType) {
        Object value = new GenericCustomPropertyReader().parse(xmlStream);
        property = new GenericProperty<Object>(propDecl, propName, value);
      } else if (propDecl instanceof EnvelopePropertyType) {
        Envelope env = null;
        xmlStream.nextTag();
        if (xmlStream.getName().equals(new QName(gmlNs, "Null"))) {
          // TODO
          StAXParsingHelper.skipElement(xmlStream);
        } else {
          env = geomReader.parseEnvelope(xmlStream, crs);
          property = new GenericProperty<Object>(propDecl, propName, env);
        }
        xmlStream.nextTag();
      } else if (propDecl instanceof CodePropertyType) {
        String codeSpace = xmlStream.getAttributeValue(null, "codeSpace");
        String code = xmlStream.getElementText().trim();
        Object value = new CodeType(code, codeSpace);
        property = new GenericProperty<Object>(propDecl, propName, value);
      } else if (propDecl instanceof MeasurePropertyType) {
        String uom = xmlStream.getAttributeValue(null, "uom");
        Object value = new Measure(xmlStream.getElementText(), uom);
        property = new GenericProperty<Object>(propDecl, propName, value);
      } else if (propDecl instanceof StringOrRefPropertyType) {
        String ref = xmlStream.getAttributeValue(CommonNamespaces.XLNNS, "href");
        String string = xmlStream.getElementText().trim();
        property = new GenericProperty<Object>(propDecl, propName, new StringOrRef(string, ref));
      }
    } else {
      LOG.trace("************ Parsing property using custom parser.");
      Object value = parser.parse(xmlStream);
      property = new GenericProperty<Object>(propDecl, propName, value);
    }

    LOG.debug(" - parsing property (end): " + xmlStream.getCurrentEventInfo());
    return property;
  }
  /**
   * Returns the object representation for the feature (or feature collection) element event that
   * the cursor of the given <code>XMLStreamReader</code> points at.
   *
   * @param xmlStream cursor must point at the <code>START_ELEMENT</code> event of the feature
   *     element, afterwards points at the next event after the <code>END_ELEMENT</code> event of
   *     the feature element
   * @param crs default CRS for all descendant geometry properties
   * @return object representation for the given feature element
   * @throws XMLStreamException
   * @throws UnknownCRSException
   * @throws XMLParsingException
   */
  public Feature parseFeature(XMLStreamReaderWrapper xmlStream, CRS crs)
      throws XMLStreamException, XMLParsingException, UnknownCRSException {

    if (schema == null) {
      schema = buildApplicationSchema(xmlStream);
    }

    Feature feature = null;
    String fid = parseFeatureId(xmlStream);

    QName featureName = xmlStream.getName();
    FeatureType ft = lookupFeatureType(xmlStream, featureName);

    LOG.debug("- parsing feature, gml:id=" + fid + " (begin): " + xmlStream.getCurrentEventInfo());

    // parse properties
    Iterator<PropertyType<?>> declIter = ft.getPropertyDeclarations(version).iterator();

    PropertyType activeDecl = declIter.next();
    int propOccurences = 0;

    CRS activeCRS = crs;
    List<Property<?>> propertyList = new ArrayList<Property<?>>();

    xmlStream.nextTag();

    while (xmlStream.getEventType() == START_ELEMENT) {

      QName propName = xmlStream.getName();

      LOG.debug("- property '" + propName + "'");

      if (findConcretePropertyType(propName, activeDecl) != null) {
        // current property element is equal to active declaration
        if (activeDecl.getMaxOccurs() != -1 && propOccurences > activeDecl.getMaxOccurs()) {
          String msg =
              Messages.getMessage(
                  "ERROR_PROPERTY_TOO_MANY_OCCURENCES",
                  propName,
                  activeDecl.getMaxOccurs(),
                  ft.getName());
          throw new XMLParsingException(xmlStream, msg);
        }
      } else {
        // current property element is not equal to active declaration
        while (declIter.hasNext() && findConcretePropertyType(propName, activeDecl) == null) {
          if (propOccurences < activeDecl.getMinOccurs()) {
            String msg = null;
            if (activeDecl.getMinOccurs() == 1) {
              msg =
                  Messages.getMessage(
                      "ERROR_PROPERTY_MANDATORY", activeDecl.getName(), ft.getName());
            } else {
              msg =
                  Messages.getMessage(
                      "ERROR_PROPERTY_TOO_FEW_OCCURENCES",
                      activeDecl.getName(),
                      activeDecl.getMinOccurs(),
                      ft.getName());
            }
            throw new XMLParsingException(xmlStream, msg);
          }
          activeDecl = declIter.next();
          propOccurences = 0;
        }
        if (findConcretePropertyType(propName, activeDecl) == null) {
          String msg = Messages.getMessage("ERROR_PROPERTY_UNEXPECTED", propName, ft.getName());
          throw new XMLParsingException(xmlStream, msg);
        }
      }

      Property<?> property =
          parseProperty(
              xmlStream, findConcretePropertyType(propName, activeDecl), activeCRS, propOccurences);
      if (property != null) {
        // if this is the "gml:boundedBy" property, override active CRS (see GML spec. (where???))
        if (StandardGMLFeatureProps.PT_BOUNDED_BY_GML31.getName().equals(activeDecl.getName())) {
          Envelope bbox = (Envelope) property.getValue();
          if (bbox.getCoordinateSystem() != null) {
            activeCRS = bbox.getCoordinateSystem();
            LOG.debug("- crs (from boundedBy): '" + activeCRS + "'");
          }
        }

        propertyList.add(property);
      }
      propOccurences++;
      xmlStream.nextTag();
    }
    LOG.debug(" - parsing feature (end): " + xmlStream.getCurrentEventInfo());

    feature = ft.newFeature(fid, propertyList, version);

    if (fid != null && !"".equals(fid)) {
      if (idContext.getObject(fid) != null) {
        String msg = Messages.getMessage("ERROR_FEATURE_ID_NOT_UNIQUE", fid);
        throw new XMLParsingException(xmlStream, msg);
      }
      idContext.addObject(feature);
    }
    return feature;
  }