hydra-spring/src/main/java/de/escalon/hypermedia/spring/hydra/LinkListSerializer.java
private void recurseSupportedProperties(JsonGenerator jgen, String currentVocab, Class<?>
            valueType, ActionDescriptor allRootParameters,
                                            ActionInputParameter rootParameter, Object currentCallValue,
                                            String propertyPath)
            throws IntrospectionException,
            IOException {


        Map<String, ActionInputParameter> properties = new HashMap<String, ActionInputParameter>();

        // collect supported properties from ctor

        Constructor[] constructors = valueType.getConstructors();
        // find default ctor
        Constructor constructor = PropertyUtils.findDefaultCtor(constructors);
        // find ctor with JsonCreator ann
        if (constructor == null) {
            constructor = PropertyUtils.findJsonCreator(constructors, JsonCreator.class);
        }
        if (constructor == null) {
            // TODO this can be a generic collection, find a way to describe it
            LOG.warn("can't describe supported properties, no default constructor or JsonCreator found for type " + valueType
                    .getName());
            return;
        }

        int parameterCount = constructor.getParameterTypes().length;
        if (parameterCount > 0) {
            Annotation[][] annotationsOnParameters = constructor.getParameterAnnotations();

            Class[] parameters = constructor.getParameterTypes();
            int paramIndex = 0;
            for (Annotation[] annotationsOnParameter : annotationsOnParameters) {
                for (Annotation annotation : annotationsOnParameter) {
                    if (JsonProperty.class == annotation.annotationType()) {
                        JsonProperty jsonProperty = (JsonProperty) annotation;
                        // TODO use required attribute of JsonProperty
                        String paramName = jsonProperty.value();

                        Object propertyValue = PropertyUtils.getPropertyOrFieldValue(currentCallValue, paramName);

                        ActionInputParameter constructorParamInputParameter =
                                new SpringActionInputParameter(
                                        new MethodParameter(constructor, paramIndex), propertyValue);

                        // TODO collect ctor params, setter params and process
                        // TODO then handle single, collection and bean for both
                        properties.put(paramName, constructorParamInputParameter);
                        paramIndex++; // increase for each @JsonProperty
                    }
                }
            }
            Assert.isTrue(parameters.length == paramIndex,
                    "not all constructor arguments of @JsonCreator " + constructor.getName() +
                            " are annotated with @JsonProperty");
        }

        // collect supported properties from setters

        // TODO support Option provider by other method args?
        final BeanInfo beanInfo = Introspector.getBeanInfo(valueType);
        final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
        // TODO collection and map
        // TODO distinguish which properties should be printed as supported - now just setters
        for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
            final Method writeMethod = propertyDescriptor.getWriteMethod();
            if (writeMethod == null) {
                continue;
            }
            // TODO: the property name must be a valid URI - need to check context for terms?
            String propertyName = getWritableExposedPropertyOrPropertyName(propertyDescriptor);

            Object propertyValue = PropertyUtils.getPropertyOrFieldValue(currentCallValue, propertyDescriptor
                    .getName());

            MethodParameter methodParameter = new MethodParameter(propertyDescriptor.getWriteMethod(), 0);
            ActionInputParameter propertySetterInputParameter = new SpringActionInputParameter(
                    methodParameter, propertyValue);

            properties.put(propertyName, propertySetterInputParameter);
        }

        // write all supported properties
        // TODO we are using the annotatedParameter.parameterName but should use the key of properties here:
        for (ActionInputParameter annotatedParameter : properties.values()) {
            String nextPropertyPathLevel = propertyPath.isEmpty() ? annotatedParameter.getParameterName() :
                    propertyPath + '.' + annotatedParameter.getParameterName();
            Class<?> parameterType = annotatedParameter.getParameterType();
            if (DataType.isSingleValueType(parameterType)) {

                final Object[] possiblePropertyValues = rootParameter.getPossibleValues(allRootParameters);

                if (rootParameter.isIncluded(nextPropertyPathLevel) && !rootParameter.isExcluded
                        (nextPropertyPathLevel)) {
                    writeSupportedProperty(jgen, currentVocab, annotatedParameter,
                            annotatedParameter.getParameterName(), possiblePropertyValues);
                }
                // TODO collections?
                //                        } else if (DataType.isArrayOrCollection(parameterType)) {
                //                            Object[] callValues = rootParameter.getValues();
                //                            int items = callValues.length;
                //                            for (int i = 0; i < items; i++) {
                //                                Object value;
                //                                if (i < callValues.length) {
                //                                    value = callValues[i];
                //                                } else {
                //                                    value = null;
                //                                }
                //                                recurseSupportedProperties(jgen, currentVocab, rootParameter
                // .getParameterType(),
                //                                        allRootParameters, rootParameter, value);
                //                            }
            } else {
                jgen.writeStartObject();
                jgen.writeStringField("hydra:property", annotatedParameter.getParameterName());
                // TODO: is the property required -> for bean props we need the Access annotation to express that

                Expose expose = AnnotationUtils.getAnnotation(parameterType, Expose.class);
                String subClass = null;
                if (expose != null) {
                    subClass = expose.value();
                } else {
                    if (List.class.isAssignableFrom(parameterType)) {
                        Type genericParameterType = annotatedParameter.getGenericParameterType();
                        if (genericParameterType instanceof ParameterizedType) {
                            ParameterizedType parameterizedType = (ParameterizedType) genericParameterType;
                            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                            if (actualTypeArguments.length == 1) {
                                Type actualTypeArgument = actualTypeArguments[0];
                                if (actualTypeArgument instanceof Class) {
                                    parameterType = (Class<?>) actualTypeArgument;
                                    subClass = parameterType.getSimpleName();
                                } else if (actualTypeArgument instanceof ParameterizedType) {
                                    ParameterizedType genericItemType = (ParameterizedType) actualTypeArgument;
                                    Type rawType = genericItemType.getRawType();
                                    if (rawType instanceof Class) {
                                        parameterType = (Class<?>) rawType;
                                        subClass = parameterType.getSimpleName();
                                    }
                                }
                            }
                        }
                        if (subClass != null) {
                            String multipleValueProp = getPropertyOrClassNameInVocab(currentVocab,
                                    "multipleValues",
                                    LdContextFactory.HTTP_SCHEMA_ORG,
                                    "schema:");
                            jgen.writeBooleanField(multipleValueProp, true);
                        }
                    }
                }
                if (subClass == null) {
                    subClass = parameterType.getSimpleName();
                }
                jgen.writeObjectFieldStart(getPropertyOrClassNameInVocab(currentVocab, "rangeIncludes",
                        LdContextFactory.HTTP_SCHEMA_ORG, "schema:"));

                jgen.writeStringField(getPropertyOrClassNameInVocab(currentVocab,
                        "subClassOf",
                        "http://www.w3.org/2000/01/rdf-schema#",
                        "rdfs:"), subClass);

                jgen.writeArrayFieldStart("hydra:supportedProperty");
                // TODO let defaultValue be an filled list, if needed
                Object propertyValue = PropertyUtils.getPropertyOrFieldValue(currentCallValue, annotatedParameter
                        .getParameterName());

                recurseSupportedProperties(jgen, currentVocab, parameterType,
                        allRootParameters,
                        rootParameter, propertyValue, nextPropertyPathLevel);
                jgen.writeEndArray();

                jgen.writeEndObject();
                jgen.writeEndObject();
            }
        }

    }
private void recurseSupportedProperties(JsonGenerator jgen, String currentVocab, Class<?>
            valueType, ActionDescriptor allRootParameters,
                                            ActionInputParameter rootParameter, Object currentCallValue,
                                            String propertyPath)
            throws IntrospectionException,
            IOException {


        Map<String, ActionInputParameter> properties = new HashMap<String, ActionInputParameter>();

        // collect supported properties from ctor

        Constructor[] constructors = valueType.getConstructors();
        // find default ctor
        Constructor constructor = PropertyUtils.findDefaultCtor(constructors);
        // find ctor with JsonCreator ann
        if (constructor == null) {
            constructor = PropertyUtils.findJsonCreator(constructors, JsonCreator.class);
        }
        if (constructor == null) {
            // TODO this can be a generic collection, find a way to describe it
            LOG.warn("can't describe supported properties, no default constructor or JsonCreator found for type " + valueType
                    .getName());
            return;
        }

        int parameterCount = constructor.getParameterTypes().length;
        if (parameterCount > 0) {
            Annotation[][] annotationsOnParameters = constructor.getParameterAnnotations();

            Class[] parameters = constructor.getParameterTypes();
            int paramIndex = 0;
            for (Annotation[] annotationsOnParameter : annotationsOnParameters) {
                for (Annotation annotation : annotationsOnParameter) {
                    if (JsonProperty.class == annotation.annotationType()) {
                        JsonProperty jsonProperty = (JsonProperty) annotation;
                        // TODO use required attribute of JsonProperty
                        String paramName = jsonProperty.value();

                        Object propertyValue = PropertyUtils.getPropertyOrFieldValue(currentCallValue, paramName);

                        ActionInputParameter constructorParamInputParameter =
                                new SpringActionInputParameter(
                                        new MethodParameter(constructor, paramIndex), propertyValue);

                        // TODO collect ctor params, setter params and process
                        // TODO then handle single, collection and bean for both
                        properties.put(paramName, constructorParamInputParameter);
                        paramIndex++; // increase for each @JsonProperty
                    }
                }
            }
            Assert.isTrue(parameters.length == paramIndex,
                    "not all constructor arguments of @JsonCreator " + constructor.getName() +
                            " are annotated with @JsonProperty");
        }

        // collect supported properties from setters

        // TODO support Option provider by other method args?
        final BeanInfo beanInfo = Introspector.getBeanInfo(valueType);
        final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
        // TODO collection and map
        // TODO distinguish which properties should be printed as supported - now just setters
        for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
            final Method writeMethod = propertyDescriptor.getWriteMethod();
            if (writeMethod == null) {
                continue;
            }
            // TODO: the property name must be a valid URI - need to check context for terms?
            String propertyName = getWritableExposedPropertyOrPropertyName(propertyDescriptor);

            Object propertyValue = PropertyUtils.getPropertyOrFieldValue(currentCallValue, propertyDescriptor
                    .getName());

            MethodParameter methodParameter = new MethodParameter(propertyDescriptor.getWriteMethod(), 0);
            ActionInputParameter propertySetterInputParameter = new SpringActionInputParameter(
                    methodParameter, propertyValue);

            properties.put(propertyName, propertySetterInputParameter);
        }

        // write all supported properties
        // TODO we are using the annotatedParameter.parameterName but should use the key of properties here:
        for (ActionInputParameter annotatedParameter : properties.values()) {
            String nextPropertyPathLevel = propertyPath.isEmpty() ? annotatedParameter.getParameterName() :
                    propertyPath + '.' + annotatedParameter.getParameterName();
            Class<?> parameterType = annotatedParameter.getParameterType();
            if (DataType.isSingleValueType(parameterType)) {

                final Object[] possiblePropertyValues = rootParameter.getPossibleValues(allRootParameters);

                if (rootParameter.isIncluded(nextPropertyPathLevel) && !rootParameter.isExcluded
                        (nextPropertyPathLevel)) {
                    writeSupportedProperty(jgen, currentVocab, annotatedParameter,
                            annotatedParameter.getParameterName(), possiblePropertyValues);
                }
                // TODO collections?
                //                        } else if (DataType.isArrayOrCollection(parameterType)) {
                //                            Object[] callValues = rootParameter.getValues();
                //                            int items = callValues.length;
                //                            for (int i = 0; i < items; i++) {
                //                                Object value;
                //                                if (i < callValues.length) {
                //                                    value = callValues[i];
                //                                } else {
                //                                    value = null;
                //                                }
                //                                recurseSupportedProperties(jgen, currentVocab, rootParameter
                // .getParameterType(),
                //                                        allRootParameters, rootParameter, value);
                //                            }
            } else {
                jgen.writeStartObject();
                jgen.writeStringField("hydra:property", annotatedParameter.getParameterName());
                // TODO: is the property required -> for bean props we need the Access annotation to express that

                Expose expose = AnnotationUtils.getAnnotation(parameterType, Expose.class);
                String subClass = null;
                if (expose != null) {
                    subClass = expose.value();
                } else {
                    if (List.class.isAssignableFrom(parameterType)) {
                        Type genericParameterType = annotatedParameter.getGenericParameterType();
                        if (genericParameterType instanceof ParameterizedType) {
                            ParameterizedType parameterizedType = (ParameterizedType) genericParameterType;
                            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                            if (actualTypeArguments.length == 1) {
                                Type actualTypeArgument = actualTypeArguments[0];
                                if (actualTypeArgument instanceof Class) {
                                    parameterType = (Class<?>) actualTypeArgument;
                                    subClass = parameterType.getSimpleName();
                                } else if (actualTypeArgument instanceof ParameterizedType) {
                                    ParameterizedType genericItemType = (ParameterizedType) actualTypeArgument;
                                    Type rawType = genericItemType.getRawType();
                                    if (rawType instanceof Class) {
                                        parameterType = (Class<?>) rawType;
                                        subClass = parameterType.getSimpleName();
                                    }
                                }
                            }
                        }
                        if (subClass != null) {
                            String multipleValueProp = getPropertyOrClassNameInVocab(currentVocab,
                                    "multipleValues",
                                    LdContextFactory.HTTP_SCHEMA_ORG,
                                    "schema:");
                            jgen.writeBooleanField(multipleValueProp, true);
                        }
                    }
                }
                if (subClass == null) {
                    subClass = parameterType.getSimpleName();
                }
                jgen.writeObjectFieldStart(getPropertyOrClassNameInVocab(currentVocab, "rangeIncludes",
                        LdContextFactory.HTTP_SCHEMA_ORG, "schema:"));

                jgen.writeStringField(getPropertyOrClassNameInVocab(currentVocab,
                        "subClassOf",
                        "http://www.w3.org/2000/01/rdf-schema#",
                        "rdfs:"), subClass);

                jgen.writeArrayFieldStart("hydra:supportedProperty");
                // TODO let defaultValue be an filled list, if needed
                Object propertyValue = PropertyUtils.getPropertyOrFieldValue(currentCallValue, annotatedParameter
                        .getParameterName());

                recurseSupportedProperties(jgen, currentVocab, parameterType,
                        allRootParameters,
                        rootParameter, propertyValue, nextPropertyPathLevel);
                jgen.writeEndArray();

                jgen.writeEndObject();
                jgen.writeEndObject();
            }
        }

    }
private void recurseSupportedProperties ( JsonGenerator jgen , String currentVocab , Class < ? > valueType , ActionDescriptor allRootParameters , ActionInputParameter rootParameter , Object currentCallValue , String propertyPath ) throws IntrospectionException , IOException { Map < String , ActionInputParameter > properties = new HashMap < String , ActionInputParameter > ( ) ; // collect supported properties from ctor Constructor [ ] constructors = valueType . getConstructors ( ) ; // find default ctor Constructor constructor = PropertyUtils . findDefaultCtor ( constructors ) ; // find ctor with JsonCreator ann if ( constructor == null ) { constructor = PropertyUtils . findJsonCreator ( constructors , JsonCreator . class ) ; } if ( constructor == null ) { // TODO this can be a generic collection, find a way to describe it LOG . warn ( "can't describe supported properties, no default constructor or JsonCreator found for type " + valueType . getName ( ) ) ; return ; } int parameterCount = constructor . getParameterTypes ( ) . length ; if ( parameterCount > 0 ) { Annotation [ ] [ ] annotationsOnParameters = constructor . getParameterAnnotations ( ) ; Class [ ] parameters = constructor . getParameterTypes ( ) ; int paramIndex = 0 ; for ( Annotation [ ] annotationsOnParameter : annotationsOnParameters ) { for ( Annotation annotation : annotationsOnParameter ) { if ( JsonProperty . class == annotation . annotationType ( ) ) { JsonProperty jsonProperty = ( JsonProperty ) annotation ; // TODO use required attribute of JsonProperty String paramName = jsonProperty . value ( ) ; Object propertyValue = PropertyUtils . getPropertyOrFieldValue ( currentCallValue , paramName ) ; ActionInputParameter constructorParamInputParameter = new SpringActionInputParameter ( new MethodParameter ( constructor , paramIndex ) , propertyValue ) ; // TODO collect ctor params, setter params and process // TODO then handle single, collection and bean for both properties . put ( paramName , constructorParamInputParameter ) ; paramIndex ++ ; // increase for each @JsonProperty } } } Assert . isTrue ( parameters . length == paramIndex , "not all constructor arguments of @JsonCreator " + constructor . getName ( ) + " are annotated with @JsonProperty" ) ; } // collect supported properties from setters // TODO support Option provider by other method args? final BeanInfo beanInfo = Introspector . getBeanInfo ( valueType ) ; final PropertyDescriptor [ ] propertyDescriptors = beanInfo . getPropertyDescriptors ( ) ; // TODO collection and map // TODO distinguish which properties should be printed as supported - now just setters for ( PropertyDescriptor propertyDescriptor : propertyDescriptors ) { final Method writeMethod = propertyDescriptor . getWriteMethod ( ) ; if ( writeMethod == null ) { continue ; } // TODO: the property name must be a valid URI - need to check context for terms? String propertyName = getWritableExposedPropertyOrPropertyName ( propertyDescriptor ) ; Object propertyValue = PropertyUtils . getPropertyOrFieldValue ( currentCallValue , propertyDescriptor . getName ( ) ) ; MethodParameter methodParameter = new MethodParameter ( propertyDescriptor . getWriteMethod ( ) , 0 ) ; ActionInputParameter propertySetterInputParameter = new SpringActionInputParameter ( methodParameter , propertyValue ) ; properties . put ( propertyName , propertySetterInputParameter ) ; } // write all supported properties // TODO we are using the annotatedParameter.parameterName but should use the key of properties here: for ( ActionInputParameter annotatedParameter : properties . values ( ) ) { String nextPropertyPathLevel = propertyPath . isEmpty ( ) ? annotatedParameter . getParameterName ( ) : propertyPath + ' ' + annotatedParameter . getParameterName ( ) ; Class < ? > parameterType = annotatedParameter . getParameterType ( ) ; if ( DataType . isSingleValueType ( parameterType ) ) { final Object [ ] possiblePropertyValues = rootParameter . getPossibleValues ( allRootParameters ) ; if ( rootParameter . isIncluded ( nextPropertyPathLevel ) && ! rootParameter . isExcluded ( nextPropertyPathLevel ) ) { writeSupportedProperty ( jgen , currentVocab , annotatedParameter , annotatedParameter . getParameterName ( ) , possiblePropertyValues ) ; } // TODO collections? //                        } else if (DataType.isArrayOrCollection(parameterType)) { //                            Object[] callValues = rootParameter.getValues(); //                            int items = callValues.length; //                            for (int i = 0; i < items; i++) { //                                Object value; //                                if (i < callValues.length) { //                                    value = callValues[i]; //                                } else { //                                    value = null; //                                } //                                recurseSupportedProperties(jgen, currentVocab, rootParameter // .getParameterType(), //                                        allRootParameters, rootParameter, value); //                            } } else { jgen . writeStartObject ( ) ; jgen . writeStringField ( "hydra:property" , annotatedParameter . getParameterName ( ) ) ; // TODO: is the property required -> for bean props we need the Access annotation to express that Expose expose = AnnotationUtils . getAnnotation ( parameterType , Expose . class ) ; String subClass = null ; if ( expose != null ) { subClass = expose . value ( ) ; } else { if ( List . class . isAssignableFrom ( parameterType ) ) { Type genericParameterType = annotatedParameter . getGenericParameterType ( ) ; if ( genericParameterType instanceof ParameterizedType ) { ParameterizedType parameterizedType = ( ParameterizedType ) genericParameterType ; Type [ ] actualTypeArguments = parameterizedType . getActualTypeArguments ( ) ; if ( actualTypeArguments . length == 1 ) { Type actualTypeArgument = actualTypeArguments [ 0 ] ; if ( actualTypeArgument instanceof Class ) { parameterType = ( Class < ? > ) actualTypeArgument ; subClass = parameterType . getSimpleName ( ) ; } else if ( actualTypeArgument instanceof ParameterizedType ) { ParameterizedType genericItemType = ( ParameterizedType ) actualTypeArgument ; Type rawType = genericItemType . getRawType ( ) ; if ( rawType instanceof Class ) { parameterType = ( Class < ? > ) rawType ; subClass = parameterType . getSimpleName ( ) ; } } } } if ( subClass != null ) { String multipleValueProp = getPropertyOrClassNameInVocab ( currentVocab , "multipleValues" , LdContextFactory . HTTP_SCHEMA_ORG , "schema:" ) ; jgen . writeBooleanField ( multipleValueProp , true ) ; } } } if ( subClass == null ) { subClass = parameterType . getSimpleName ( ) ; } jgen . writeObjectFieldStart ( getPropertyOrClassNameInVocab ( currentVocab , "rangeIncludes" , LdContextFactory . HTTP_SCHEMA_ORG , "schema:" ) ) ; jgen . writeStringField ( getPropertyOrClassNameInVocab ( currentVocab , "subClassOf" , "http://www.w3.org/2000/01/rdf-schema#" , "rdfs:" ) , subClass ) ; jgen . writeArrayFieldStart ( "hydra:supportedProperty" ) ; // TODO let defaultValue be an filled list, if needed Object propertyValue = PropertyUtils . getPropertyOrFieldValue ( currentCallValue , annotatedParameter . getParameterName ( ) ) ; recurseSupportedProperties ( jgen , currentVocab , parameterType , allRootParameters , rootParameter , propertyValue , nextPropertyPathLevel ) ; jgen . writeEndArray ( ) ; jgen . writeEndObject ( ) ; jgen . writeEndObject ( ) ; } } }
Writes bean description recursively.

@param jgen              to write to
@param currentVocab      in context
@param valueType         class of value
@param allRootParameters of the method that receives the request body
@param rootParameter     the request body
@param currentCallValue  the value at the current recursion level
@param propertyPath      of the current recursion level
@throws IntrospectionException
@throws IOException
Writes bean description recursively .