/*
* Copyright (c) 2014. Escalon System-Entwicklung, Dietrich Schulten
*
* Licensed 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 de.escalon.hypermedia.spring.hydra;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import de.escalon.hypermedia.PropertyUtils;
import de.escalon.hypermedia.action.Cardinality;
import de.escalon.hypermedia.action.Input;
import de.escalon.hypermedia.affordance.*;
import de.escalon.hypermedia.hydra.mapping.Expose;
import de.escalon.hypermedia.hydra.serialize.JacksonHydraSerializer;
import de.escalon.hypermedia.hydra.serialize.JsonLdKeywords;
import de.escalon.hypermedia.hydra.serialize.LdContext;
import de.escalon.hypermedia.hydra.serialize.LdContextFactory;
import de.escalon.hypermedia.spring.SpringActionInputParameter;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.hateoas.IanaRels;
import org.springframework.hateoas.Link;
import org.springframework.util.Assert;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
/**
* Serializer to convert Link to json-ld representation. Created by dschulten on 19.09.2014.
*/
public class LinkListSerializer extends StdSerializer<List<Link>> {
Logger LOG = LoggerFactory.getLogger(LinkListSerializer.class);
private static final String IANA_REL_PREFIX = "urn:iana:link-relations:";
public LinkListSerializer() {
super(List.class, false);
}
@Override
public void serialize(List<Link> links, JsonGenerator jgen,
SerializerProvider serializerProvider) throws IOException {
try {
Collection<Link> simpleLinks = new ArrayList<Link>();
Collection<Affordance> affordances = new ArrayList<Affordance>();
Collection<Link> templatedLinks = new ArrayList<Link>();
Collection<Affordance> collectionAffordances = new ArrayList<Affordance>();
Link selfRel = null;
for (Link link : links) {
if (link instanceof Affordance) {
final Affordance affordance = (Affordance) link;
final List<ActionDescriptor> actionDescriptors = affordance.getActionDescriptors();
if (!actionDescriptors.isEmpty()) {
// TODO: consider to use Link href for template even if it is not compatible
if (affordance.getUriTemplateComponents()
.hasVariables()) {
// TODO resolve rel against context
if ("hydra:search".equals(affordance.getRel())
|| Cardinality.SINGLE == affordance
.getCardinality()) {
templatedLinks.add(affordance);
} else {
collectionAffordances.add(affordance);
}
} else {
// if all required variables are satisfied, the url can be used as identifier
// by stripping optional variables
if (!affordance.isSelfRel() && Cardinality.COLLECTION == affordance.getCardinality()) {
collectionAffordances.add(affordance);
} else {
affordances.add(affordance);
}
}
} else {
if (affordance.isTemplated()) {
templatedLinks.add(affordance);
} else {
simpleLinks.add(affordance);
}
}
} else if (link.isTemplated()) {
templatedLinks.add(link);
} else {
simpleLinks.add(link);
}
if ("self".equals(link.getRel())) {
selfRel = link;
}
}
for (Link templatedLink : templatedLinks) {
// templated affordance might turn out to have all variables satisfied or
// only optional unsatisfied variables
ActionDescriptor actionDescriptorForHttpGet = getActionDescriptorForHttpGet(templatedLink);
// TODO handle rev here
String rel = templatedLink.getRel();
writeIriTemplate(rel, templatedLink.getHref(), templatedLink.getVariableNames(),
actionDescriptorForHttpGet, jgen);
}
@SuppressWarnings("unchecked")
Deque<LdContext> contextStack = (Deque<LdContext>) serializerProvider.getAttribute(JacksonHydraSerializer
.KEY_LD_CONTEXT);
String currentVocab = (contextStack != null && !contextStack.isEmpty()) ?
contextStack.peek().vocab : null;
// related collections
if (!collectionAffordances.isEmpty()) {
jgen.writeArrayFieldStart("hydra:collection");
for (Affordance collectionAffordance : collectionAffordances) {
jgen.writeStartObject();
jgen.writeStringField(JsonLdKeywords.AT_TYPE, "hydra:Collection");
PartialUriTemplateComponents templateComponents =
collectionAffordance.getUriTemplateComponents();
if (!collectionAffordance.isBaseUriTemplated() &&
!collectionAffordance.hasUnsatisfiedRequiredVariables()) {
String collectionUri = templateComponents.getBaseUri()
+ templateComponents.getQueryHead();
jgen.writeStringField(JsonLdKeywords.AT_ID, collectionUri);
}
if (templateComponents.hasVariables()) {
ActionDescriptor actionDescriptorForHttpGet = getActionDescriptorForHttpGet
(collectionAffordance);
writeIriTemplate("hydra:search", templateComponents.toString(),
templateComponents.getVariableNames(), actionDescriptorForHttpGet,
jgen);
}
jgen.writeObjectFieldStart("hydra:manages");
// do we have a collection holder which is not owner of the affordance?
TypedResource collectionHolder = collectionAffordance.getCollectionHolder();
if (collectionAffordance.getRev() != null) {
jgen.writeStringField("hydra:property", collectionAffordance.getRev());
if (collectionHolder != null) {
// can't use writeObjectField, it won't inherit the context stack
writeCollectionHolder("hydra:object", collectionHolder, jgen);
} else if (selfRel != null) {
jgen.writeStringField("hydra:object", selfRel.getHref());
}
} else if (collectionAffordance.getRel() != null) {
jgen.writeStringField("hydra:property", collectionAffordance.getRel());
if (collectionHolder != null) {
// can't use writeObjectField, it won't inherit the context stack
writeCollectionHolder("hydra:subject", collectionHolder, jgen);
} else if (selfRel != null) {
jgen.writeStringField("hydra:subject", selfRel.getHref());
}
}
jgen.writeEndObject(); // end manages
List<ActionDescriptor> actionDescriptors = collectionAffordance.getActionDescriptors();
if (!actionDescriptors.isEmpty()) {
jgen.writeArrayFieldStart("hydra:operation");
}
writeActionDescriptors(jgen, currentVocab, actionDescriptors);
if (!actionDescriptors.isEmpty()) {
jgen.writeEndArray(); // end hydra:operation
}
jgen.writeEndObject(); // end collection
}
jgen.writeEndArray();
}
for (Affordance affordance : affordances) {
final String rel = affordance.getRel();
List<ActionDescriptor> actionDescriptors = affordance.getActionDescriptors();
if (!actionDescriptors.isEmpty()) {
if (!Link.REL_SELF.equals(rel)) {
jgen.writeObjectFieldStart(rel); // begin rel
}
jgen.writeStringField(JsonLdKeywords.AT_ID, affordance.getHref());
jgen.writeArrayFieldStart("hydra:operation");
}
writeActionDescriptors(jgen, currentVocab, actionDescriptors);
if (!actionDescriptors.isEmpty()) {
jgen.writeEndArray(); // end hydra:operation
if (!Link.REL_SELF.equals(rel)) {
jgen.writeEndObject(); // end rel
}
}
}
for (Link simpleLink : simpleLinks) {
final String rel = simpleLink.getRel();
if (Link.REL_SELF.equals(rel)) {
jgen.writeStringField("@id", simpleLink.getHref());
} else {
String linkAttributeName = IanaRels.isIanaRel(rel) ? IANA_REL_PREFIX + rel : rel;
jgen.writeObjectFieldStart(linkAttributeName);
jgen.writeStringField("@id", simpleLink.getHref());
jgen.writeEndObject();
}
}
} catch (IntrospectionException e) {
throw new RuntimeException(e);
}
}
private void writeIriTemplate(String rel, String href, List<String> variableNames, ActionDescriptor
actionDescriptorForHttpGet,
JsonGenerator jgen) throws IOException {
jgen.writeObjectFieldStart(rel);
jgen.writeStringField("@type", "hydra:IriTemplate");
jgen.writeStringField("hydra:template", href);
jgen.writeArrayFieldStart("hydra:mapping");
writeHydraVariableMapping(jgen, actionDescriptorForHttpGet, variableNames);
jgen.writeEndArray();
jgen.writeEndObject();
}
private void writeCollectionHolder(String fieldName, TypedResource collectionHolder, JsonGenerator jgen) throws
IOException {
jgen.writeObjectFieldStart(fieldName);
String identifyingUri = collectionHolder.getIdentifyingUri();
if (identifyingUri != null) {
jgen.writeStringField(JsonLdKeywords.AT_ID, identifyingUri);
}
jgen.writeStringField(JsonLdKeywords.AT_TYPE, collectionHolder.getSemanticType());
jgen.writeEndObject();
}
@Nullable
private ActionDescriptor getActionDescriptorForHttpGet(Link templatedAffordance) {
if (!(templatedAffordance instanceof Affordance)) {
return null;
}
final List<ActionDescriptor> actionDescriptors = ((Affordance) templatedAffordance).getActionDescriptors();
ActionDescriptor actionDescriptorGet = null;
for (ActionDescriptor actionDescriptor : actionDescriptors) {
String httpMethod = actionDescriptor.getHttpMethod();
if ("GET".equalsIgnoreCase(httpMethod)) {
actionDescriptorGet = actionDescriptor;
}
}
return actionDescriptorGet;
}
private void writeActionDescriptors(JsonGenerator jgen, String currentVocab, List<ActionDescriptor>
actionDescriptors) throws IOException, IntrospectionException {
for (ActionDescriptor actionDescriptor : actionDescriptors) {
if ("GET".equals(actionDescriptor.getHttpMethod())) {
continue;
}
jgen.writeStartObject(); // begin a hydra:Operation
final String semanticActionType = actionDescriptor.getSemanticActionType();
if (semanticActionType != null) {
jgen.writeStringField("@type", semanticActionType);
}
jgen.writeStringField("hydra:method", actionDescriptor.getHttpMethod());
final ActionInputParameter requestBodyInputParameter = actionDescriptor.getRequestBody();
if (requestBodyInputParameter != null) {
jgen.writeObjectFieldStart("hydra:expects"); // begin hydra:expects
final Class<?> clazz = requestBodyInputParameter.getParameterType();
final Expose classExpose = clazz.getAnnotation(Expose.class);
final String typeName;
if (classExpose != null) {
typeName = classExpose.value();
} else {
typeName = requestBodyInputParameter.getParameterType()
.getSimpleName();
}
jgen.writeStringField("@type", typeName);
jgen.writeArrayFieldStart("hydra:supportedProperty"); // begin hydra:supportedProperty
// TODO check need for allRootParameters and requestBodyInputParameter here:
recurseSupportedProperties(jgen, currentVocab, clazz, actionDescriptor,
requestBodyInputParameter, requestBodyInputParameter.getValue(), "");
jgen.writeEndArray(); // end hydra:supportedProperty
jgen.writeEndObject(); // end hydra:expects
}
jgen.writeEndObject(); // end hydra:Operation
}
}
/**
* 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
*/
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();
}
}
}
/**
* Gets property or class name in the current context, either without prefix if the current vocab is the given
* vocabulary, or prefixed otherwise.
*
* @param currentVocab to determine the current vocab
* @param propertyOrClassName name to contextualize
* @param vocabulary to which the given property belongs
* @param vocabularyPrefixWithColon to use if the current vocab does not match the given vocabulary to which the name belongs, should end
* with colon
* @return property name or class name in the currenct context
*/
private String getPropertyOrClassNameInVocab(@Nullable String currentVocab, String propertyOrClassName, String
vocabulary, String vocabularyPrefixWithColon) {
Assert.notNull(vocabulary);
String ret;
if (vocabulary.equals(currentVocab)) {
ret = propertyOrClassName;
} else {
ret = vocabularyPrefixWithColon + propertyOrClassName;
}
return ret;
}
private void writeSupportedProperty(JsonGenerator jgen, String currentVocab,
ActionInputParameter actionInputParameter,
String propertyName, @SuppressWarnings("unused") Object[]
possiblePropertyValues)
throws IOException {
jgen.writeStartObject();
if (actionInputParameter.hasValue() || actionInputParameter.hasInputConstraints()) {
// jgen.writeArrayFieldStart("@type");
// jgen.writeString("hydra:SupportedProperty");
jgen.writeStringField(JsonLdKeywords.AT_TYPE, getPropertyOrClassNameInVocab(currentVocab,
"PropertyValueSpecification", LdContextFactory.HTTP_SCHEMA_ORG, "schema:"));
//jgen.writeEndArray();
}
jgen.writeStringField("hydra:property", propertyName);
writePossiblePropertyValues(jgen, currentVocab, actionInputParameter, possiblePropertyValues);
jgen.writeEndObject();
}
private void writePossiblePropertyValues(JsonGenerator jgen, String currentVocab, ActionInputParameter
actionInputParameter, @SuppressWarnings("unused") Object[] possiblePropertyValues) throws IOException {
// Enable the following to list possible values.
// Problem: how to express individuals only for certain hydra:options
// not all hydra:options should be taken as uris, sometimes they might be just literals
// how to make that clear to the client?
// maybe we must write them out for options
// if (possiblePropertyValues.length > 0) {
// jgen.writeArrayFieldStart("hydra:option");
//
// for (Object possibleValue : possiblePropertyValues) {
// // TODO: apply "hydra:option" : { "@type": "@vocab"} to context for enums
// writeScalarValue(jgen, possibleValue, rootParameter.getParameterType());
// }
// jgen.writeEndArray();
// }
if (actionInputParameter.isArrayOrCollection()) {
jgen.writeBooleanField(getPropertyOrClassNameInVocab(currentVocab, "multipleValues",
LdContextFactory.HTTP_SCHEMA_ORG, "schema:"), true);
}
// valueRequired (hard to say, using @Access on Event is for all update requests - or make
// specific request beans for different
// purposes rather than always passing an instance of e.g. Event?)
// -> update is a different use case than create - or maybe have an @Requires("eventStatus")
// annotation alongside requestBody to tell which attributes are required or writable, and use
// Requires over
// bean structure, where ctor with least length of args is required and setters are supported
// but optional? The bean structure does say what is writable for updates, but not what is required
// for creation. Right now setters are supportedProperties. For creation we would have to add constructor
// arguments as supportedProperties.
// (/) defaultValue (pre-filled value, e.g. list of selected items for option)
// valueName (for iri templates only)
// (/) readonlyValue (true for final public field or absence of setter, send fixed value like hidden field?)
// -> use hydra:readable, hydra:writable
// (/) multipleValues
// (/) valueMinLength
// (/) valueMaxLength
// (/) valuePattern
// minValue (DateTime support)
// maxValue (DateTime support)
// (/) stepValue
final Map<String, Object> inputConstraints = actionInputParameter.getInputConstraints();
if (actionInputParameter.hasValue()) {
if (actionInputParameter.isArrayOrCollection()) {
Object[] callValues = actionInputParameter.getValues();
Class<?> componentType = callValues.getClass()
.getComponentType();
// only write defaultValue for array of scalars
if (DataType.isSingleValueType(componentType)) {
jgen.writeFieldName(getPropertyOrClassNameInVocab(currentVocab, "defaultValue",
LdContextFactory.HTTP_SCHEMA_ORG, "schema:"));
jgen.writeStartArray();
for (Object callValue : callValues) {
writeScalarValue(jgen, callValue, componentType);
}
jgen.writeEndArray();
}
} else {
jgen.writeFieldName(getPropertyOrClassNameInVocab(currentVocab, "defaultValue",
LdContextFactory.HTTP_SCHEMA_ORG, "schema:"));
writeScalarValue(jgen, actionInputParameter.getValue(), actionInputParameter
.getParameterType());
}
}
if (!inputConstraints.isEmpty()) {
final List<String> keysToAppendValue = Arrays.asList(Input.MAX, Input.MIN,
Input.STEP);
for (String keyToAppendValue : keysToAppendValue) {
final Object constraint = inputConstraints.get(keyToAppendValue);
if (constraint != null) {
jgen.writeFieldName(getPropertyOrClassNameInVocab(currentVocab, keyToAppendValue + "Value",
LdContextFactory.HTTP_SCHEMA_ORG, "schema:"));
jgen.writeNumber(constraint
.toString());
}
}
final List<String> keysToPrependValue = Arrays.asList(Input.MAX_LENGTH,
Input.MIN_LENGTH, Input.PATTERN);
for (String keyToPrependValue : keysToPrependValue) {
final Object constraint = inputConstraints.get(keyToPrependValue);
if (constraint != null) {
jgen.writeFieldName(getPropertyOrClassNameInVocab(currentVocab, "value" + StringUtils.capitalize
(keyToPrependValue),
LdContextFactory.HTTP_SCHEMA_ORG, "schema:"));
if (Input.PATTERN.equals(keyToPrependValue)) {
jgen.writeString(constraint.toString());
} else {
jgen.writeNumber(constraint
.toString());
}
}
}
}
}
private void writeScalarValue(JsonGenerator jgen, Object possibleValue,
Class<?> valueType) throws IOException {
if (Number.class.isAssignableFrom(valueType)) {
jgen.writeNumber(possibleValue.toString());
} else if (Boolean.class.isAssignableFrom(valueType)) {
jgen.writeBoolean((Boolean) possibleValue);
} else if (Enum.class.isAssignableFrom(valueType)) {
jgen.writeString(((Enum) possibleValue).name());
} else {
jgen.writeString(possibleValue.toString());
}
}
// private boolean isSelected(Object possibleValue, ActionInputParameter rootParameter) {
// boolean ret;
// if (rootParameter.isArrayOrCollection()) {
// ret = ArrayUtils.contains(rootParameter.getValues(), possibleValue);
// } else {
// final Object callValue = rootParameter.getValue();
// ret = (callValue == null ? false :
// callValue.equals(possibleValue));
// }
// return ret;
// }
// private void writePropertyValueSpecifications(JsonGenerator jgen,
// ActionDescriptor allRootParameters) throws IOException {
// // TODO use input constraints
// for (String pathVariableName : allRootParameters.getPathVariableNames()) {
// jgen.writeStringField(pathVariableName + "-input", "required");
// }
// for (String requestParamName : allRootParameters.getRequestParamNames()) {
// // TODO could be a list -> tell the client using select options, but what about a list
// // of free length, such as ids?
// jgen.writeStringField(requestParamName + "-input", "required");
// }
// }
// private void writeSimpleTarget(JsonGenerator jgen, Link action, Affordance affordance) throws IOException {
// jgen.writeStringField("target", action.getHref());
// }
//
// private void writeEntryPointTarget(JsonGenerator jgen, Link action, Affordance affordance) throws IOException {
// jgen.writeObjectFieldStart("target");
// jgen.writeStringField("@type", "EntryPoint");
// jgen.writeStringField("urlTemplate", action.getHref());
// List<ActionDescriptor> actionDescriptors = affordance.getActionDescriptors();
// if (actionDescriptors != null && actionDescriptors.getHttpMethod() != null) {
// jgen.writeStringField("httpMethod", actionDescriptors.getHttpMethod().name());
// }
// // TODO encodingType, contentType, application
// jgen.writeEndObject();
// }
private void writeHydraVariableMapping(JsonGenerator jgen, @Nullable ActionDescriptor annotatedParameters,
Collection<String> variableNames) throws IOException {
if (annotatedParameters != null) {
for (String variableName : variableNames) {
// TODO: find also @Input
ActionInputParameter annotatedParameter = annotatedParameters.getActionInputParameter(variableName);
// TODO access @Input parameter, too
// only unsatisfied parameters become hydra variables
if (annotatedParameter != null && annotatedParameter.getValue() == null) {
jgen.writeStartObject();
jgen.writeStringField("@type", "hydra:IriTemplateMapping");
jgen.writeStringField("hydra:variable", variableName);
jgen.writeBooleanField("hydra:required",
annotatedParameter
.isRequired());
jgen.writeStringField("hydra:property",
getExposedPropertyOrParamName(annotatedParameter));
jgen.writeEndObject();
}
}
}
}
/**
* Gets exposed property or parameter name.
*
* @param inputParameter for exposure
* @return property name
*/
private String getExposedPropertyOrParamName(ActionInputParameter inputParameter) {
final Expose expose = inputParameter.getAnnotation(Expose.class);
String property;
if (expose != null) {
property = expose.value();
} else {
property = inputParameter.getParameterName();
}
return property;
}
/**
* Gets exposed property or parameter name for properties with an appropriate setter (=write) method.
*
* @param inputParameter for exposure
* @return property name
*/
private String getWritableExposedPropertyOrPropertyName(PropertyDescriptor inputParameter) {
final Method writeMethod = inputParameter.getWriteMethod();
final Expose expose = writeMethod
.getAnnotation(Expose.class);
String propertyName;
if (expose != null) {
propertyName = expose.value();
} else {
propertyName = inputParameter.getName();
}
return propertyName;
}
@Override
public boolean isUnwrappingSerializer() {
return true;
}
}