/**
  * Special handling for polymorphic mapping. Works out the polymorphic type name by evaluating the
  * function on the feature, then set the relevant sub-type values.
  *
  * @param target The target feature to be encoded
  * @param id The target feature id
  * @param nestedMapping The mapping that is polymorphic
  * @param source The source simple feature
  * @param xpath The xpath of polymorphic type
  * @param clientPropsMappings Client properties
  * @throws IOException
  */
 private Attribute setPolymorphicValues(
     Name mappingName,
     Attribute target,
     String id,
     NestedAttributeMapping nestedMapping,
     Object source,
     StepList xpath,
     Map<Name, Expression> clientPropsMappings)
     throws IOException {
   // process sub-type mapping
   DataAccess<FeatureType, Feature> da = DataAccessRegistry.getDataAccess((Name) mappingName);
   if (da instanceof AppSchemaDataAccess) {
     // why wouldn't it be? check just to be safe
     FeatureTypeMapping fTypeMapping =
         ((AppSchemaDataAccess) da).getMappingByName((Name) mappingName);
     List<AttributeMapping> polymorphicMappings = fTypeMapping.getAttributeMappings();
     AttributeDescriptor attDescriptor = fTypeMapping.getTargetFeature();
     AttributeType type = attDescriptor.getType();
     Name polymorphicTypeName = attDescriptor.getName();
     StepList prefixedXpath = xpath.clone();
     prefixedXpath.add(
         new Step(
             new QName(
                 polymorphicTypeName.getNamespaceURI(),
                 polymorphicTypeName.getLocalPart(),
                 this.namespaces.getPrefix(polymorphicTypeName.getNamespaceURI())),
             1));
     if (!fTypeMapping.getFeatureIdExpression().equals(Expression.NIL)) {
       id = fTypeMapping.getFeatureIdExpression().evaluate(source, String.class);
     }
     Attribute instance =
         xpathAttributeBuilder.set(
             target, prefixedXpath, null, id, type, false, attDescriptor, null);
     setClientProperties(instance, source, clientPropsMappings);
     for (AttributeMapping mapping : polymorphicMappings) {
       if (skipTopElement(polymorphicTypeName, mapping, type)) {
         // if the top level mapping for the Feature itself, the attribute instance
         // has already been created.. just need to set the client properties
         setClientProperties(instance, source, mapping.getClientProperties());
         continue;
       }
       setAttributeValue(
           instance, null, source, mapping, null, null, selectedProperties.get(mapping));
     }
     return instance;
   }
   return null;
 }
  /**
   * Sets the values of grouping attributes.
   *
   * @param target
   * @param source
   * @param attMapping
   * @param values
   * @return Feature. Target feature sets with simple attributes
   */
  protected Attribute setAttributeValue(
      Attribute target,
      String id,
      final Object source,
      final AttributeMapping attMapping,
      Object values,
      StepList inputXpath,
      List<PropertyName> selectedProperties)
      throws IOException {

    final Expression sourceExpression = attMapping.getSourceExpression();
    final AttributeType targetNodeType = attMapping.getTargetNodeInstance();
    StepList xpath = inputXpath == null ? attMapping.getTargetXPath().clone() : inputXpath;

    Map<Name, Expression> clientPropsMappings = attMapping.getClientProperties();
    boolean isNestedFeature = attMapping.isNestedAttribute();

    if (id == null && Expression.NIL != attMapping.getIdentifierExpression()) {
      id = extractIdForAttribute(attMapping.getIdentifierExpression(), source);
    }
    if (attMapping.isNestedAttribute()) {
      NestedAttributeMapping nestedMapping = ((NestedAttributeMapping) attMapping);
      Object mappingName = nestedMapping.getNestedFeatureType(source);
      if (mappingName != null) {
        if (nestedMapping.isSameSource() && mappingName instanceof Name) {
          // data type polymorphism mapping
          return setPolymorphicValues(
              (Name) mappingName, target, id, nestedMapping, source, xpath, clientPropsMappings);
        } else if (mappingName instanceof String) {
          // referential polymorphism mapping
          return setPolymorphicReference(
              (String) mappingName, clientPropsMappings, target, xpath, targetNodeType);
        }
      } else {
        // polymorphism could result in null, to skip the attribute
        return null;
      }
    }
    if (values == null && source != null) {
      values = getValues(attMapping.isMultiValued(), sourceExpression, source);
    }
    boolean isHRefLink = isByReference(clientPropsMappings, isNestedFeature);
    if (isNestedFeature) {
      if (values == null) {
        // polymorphism use case, if the value doesn't match anything, don't encode
        return null;
      }
      // get built feature based on link value
      if (values instanceof Collection) {
        ArrayList<Attribute> nestedFeatures =
            new ArrayList<Attribute>(((Collection) values).size());
        for (Object val : (Collection) values) {
          if (val instanceof Attribute) {
            val = ((Attribute) val).getValue();
            if (val instanceof Collection) {
              val = ((Collection) val).iterator().next();
            }
            while (val instanceof Attribute) {
              val = ((Attribute) val).getValue();
            }
          }
          if (isHRefLink) {
            // get the input features to avoid infinite loop in case the nested
            // feature type also have a reference back to this type
            // eg. gsml:GeologicUnit/gsml:occurence/gsml:MappedFeature
            // and gsml:MappedFeature/gsml:specification/gsml:GeologicUnit
            nestedFeatures.addAll(
                ((NestedAttributeMapping) attMapping)
                    .getInputFeatures(
                        this,
                        val,
                        getIdValues(source),
                        source,
                        reprojection,
                        selectedProperties,
                        includeMandatory));
          } else {
            nestedFeatures.addAll(
                ((NestedAttributeMapping) attMapping)
                    .getFeatures(
                        this,
                        val,
                        getIdValues(source),
                        reprojection,
                        source,
                        selectedProperties,
                        includeMandatory));
          }
        }
        values = nestedFeatures;
      } else if (isHRefLink) {
        // get the input features to avoid infinite loop in case the nested
        // feature type also have a reference back to this type
        // eg. gsml:GeologicUnit/gsml:occurence/gsml:MappedFeature
        // and gsml:MappedFeature/gsml:specification/gsml:GeologicUnit
        values =
            ((NestedAttributeMapping) attMapping)
                .getInputFeatures(
                    this,
                    values,
                    getIdValues(source),
                    source,
                    reprojection,
                    selectedProperties,
                    includeMandatory);
      } else {
        values =
            ((NestedAttributeMapping) attMapping)
                .getFeatures(
                    this,
                    values,
                    getIdValues(source),
                    reprojection,
                    source,
                    selectedProperties,
                    includeMandatory);
      }
      if (isHRefLink) {
        // only need to set the href link value, not the nested feature properties
        setXlinkReference(target, clientPropsMappings, values, xpath, targetNodeType);
        return null;
      }
    }
    Attribute instance = null;
    if (values instanceof Collection) {
      // nested feature type could have multiple instances as the whole purpose
      // of feature chaining is to cater for multi-valued properties
      for (Object singleVal : (Collection) values) {
        ArrayList valueList = new ArrayList();
        // copy client properties from input features if they're complex features
        // wrapped in app-schema data access
        if (singleVal instanceof Attribute) {
          // copy client properties from input features if they're complex features
          // wrapped in app-schema data access
          Map<Name, Expression> valueProperties = getClientProperties((Attribute) singleVal);
          if (!valueProperties.isEmpty()) {
            clientPropsMappings.putAll(valueProperties);
          }
        }
        if (!isNestedFeature) {
          if (singleVal instanceof Attribute) {
            singleVal = ((Attribute) singleVal).getValue();
          }
          if (singleVal instanceof Collection) {
            valueList.addAll((Collection) singleVal);
          } else {
            valueList.add(singleVal);
          }
        } else {
          valueList.add(singleVal);
        }
        instance =
            xpathAttributeBuilder.set(
                target, xpath, valueList, id, targetNodeType, false, sourceExpression);
        setClientProperties(instance, source, clientPropsMappings);
      }
    } else {
      if (values instanceof Attribute) {
        // copy client properties from input features if they're complex features
        // wrapped in app-schema data access
        Map<Name, Expression> newClientProps = getClientProperties((Attribute) values);
        if (!newClientProps.isEmpty()) {
          newClientProps.putAll(clientPropsMappings);
          clientPropsMappings = newClientProps;
        }
        values = ((Attribute) values).getValue();
      }
      instance =
          xpathAttributeBuilder.set(
              target, xpath, values, id, targetNodeType, false, sourceExpression);
      setClientProperties(instance, source, clientPropsMappings);
    }
    if (instance != null && attMapping.encodeIfEmpty()) {
      instance.getDescriptor().getUserData().put("encodeIfEmpty", attMapping.encodeIfEmpty());
    }
    return instance;
  }