/*
* 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.hydra.serialize;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.impl.BeanAsArraySerializer;
import com.fasterxml.jackson.databind.ser.impl.ObjectIdWriter;
import com.fasterxml.jackson.databind.ser.std.BeanSerializerBase;
import com.fasterxml.jackson.databind.util.NameTransformer;
import de.escalon.hypermedia.hydra.mapping.Expose;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.Map;
import static de.escalon.hypermedia.AnnotationUtils.findAnnotation;
public class JacksonHydraSerializer extends BeanSerializerBase {
public static final String KEY_LD_CONTEXT = "de.escalon.hypermedia.ld-context";
protected LdContextFactory ldContextFactory;
private ProxyUnwrapper proxyUnwrapper;
public JacksonHydraSerializer(BeanSerializerBase source) {
this(source, (ProxyUnwrapper) null);
}
/**
* Creates new serializer with optional proxy unwrapper.
*
* @param source
* wrapped serializer
* @param proxyUnwrapper
* to unwrap proxified beans, may be null
*/
public JacksonHydraSerializer(BeanSerializerBase source, ProxyUnwrapper proxyUnwrapper) {
super(source);
this.proxyUnwrapper = proxyUnwrapper;
this.ldContextFactory = new LdContextFactory();
ldContextFactory.setProxyUnwrapper(proxyUnwrapper);
}
public JacksonHydraSerializer(BeanSerializerBase source,
ObjectIdWriter objectIdWriter) {
super(source, objectIdWriter);
}
public JacksonHydraSerializer(BeanSerializerBase source,
String[] toIgnore) {
super(source, toIgnore);
}
public BeanSerializerBase withObjectIdWriter(
ObjectIdWriter objectIdWriter) {
return new JacksonHydraSerializer(this, objectIdWriter);
}
protected BeanSerializerBase withIgnorals(String[] toIgnore) {
return new JacksonHydraSerializer(this, toIgnore);
}
@Override
protected BeanSerializerBase asArraySerializer() {
/* Can not:
*
* - have Object Id (may be allowed in future)
* - have any getter
*
*/
if ((_objectIdWriter == null)
&& (_anyGetterWriter == null)
&& (_propertyFilterId == null)
) {
return new BeanAsArraySerializer(this);
}
// already is one, so:
return this;
}
@Override
protected BeanSerializerBase withFilterId(Object filterId) {
final JacksonHydraSerializer ret = new JacksonHydraSerializer(this);
ret.withFilterId(filterId);
return ret;
}
@Override
public void serialize(Object bean, JsonGenerator jgen,
SerializerProvider serializerProvider) throws IOException {
if (!isUnwrappingSerializer()) {
jgen.writeStartObject();
}
Deque<LdContext> contextStack = (Deque<LdContext>) serializerProvider.getAttribute(KEY_LD_CONTEXT);
if (contextStack == null) {
contextStack = new ArrayDeque<LdContext>();
serializerProvider.setAttribute(KEY_LD_CONTEXT, contextStack);
}
serializeContext(bean, jgen, serializerProvider, contextStack);
serializeType(bean, jgen, serializerProvider);
serializeFields(bean, jgen, serializerProvider);
if (!isUnwrappingSerializer()) {
jgen.writeEndObject();
}
contextStack = (Deque<LdContext>) serializerProvider.getAttribute(KEY_LD_CONTEXT);
if (!contextStack.isEmpty()) {
contextStack.pop();
}
}
protected void serializeType(Object bean, JsonGenerator jgen, SerializerProvider provider) throws IOException {
if (proxyUnwrapper != null) {
bean = proxyUnwrapper.unwrapProxy(bean);
}
// adds @type attribute, reflecting the simple name of the class or the exposed annotation on the class.
final Expose classExpose = findAnnotation(bean.getClass(), Expose.class);
// TODO allow to search up the hierarchy for ResourceSupport mixins and cache found result?
final Class<?> mixin = provider.getConfig()
.findMixInClassFor(bean.getClass());
final Expose mixinExpose = findAnnotation(mixin, Expose.class);
final String val;
if (mixinExpose != null) {
val = mixinExpose.value(); // mixin wins over class
} else if (classExpose != null) {
val = classExpose.value(); // expose is better than Java type
} else {
val = bean.getClass()
.getSimpleName();
}
jgen.writeStringField(JsonLdKeywords.AT_TYPE, val);
}
protected void serializeContext(Object bean, JsonGenerator jgen,
SerializerProvider serializerProvider, Deque<LdContext> contextStack)
throws IOException {
// TODO: this code is duplicated in PagedResourcesSerializer!!!
// couldn't inherit from this because this is a serializer wrapper
// make it a static utility or a common collaborator dependency?
if (proxyUnwrapper != null) {
bean = proxyUnwrapper.unwrapProxy(bean);
}
MixinSource mixinSource = new JacksonMixinSource(serializerProvider.getConfig());
final Class<?> mixInClass = mixinSource.findMixInClassFor(bean.getClass());
final LdContext parentContext = contextStack.peek();
Map<String, Object> termsOfBean = ldContextFactory.getTerms(mixinSource, bean, mixInClass);
Map<String, Object> newTermsOfBean;
if (parentContext != null) {
newTermsOfBean = new LinkedHashMap<String, Object>();
for (Map.Entry<String, Object> termEntry : termsOfBean.entrySet()) {
String term = termEntry.getKey();
Object value = termEntry.getValue();
if (!parentContext.hasEqualTerm(term, value)) {
newTermsOfBean.put(term, value);
}
}
} else {
newTermsOfBean = termsOfBean;
}
LdContext currentContext = new LdContext(parentContext, ldContextFactory.getVocab(mixinSource, bean,
mixInClass), newTermsOfBean);
contextStack.push(currentContext);
// check if we need to write a context for the current bean at all
// If it is in the same vocab: no context
// If the terms are already defined in the context: no context
boolean mustWriteContext;
if (parentContext == null || !parentContext.contains(currentContext)) {
mustWriteContext = true;
} else {
mustWriteContext = false;
}
if (mustWriteContext) {
// begin context
// default context: schema.org vocab or vocab package annotation
jgen.writeObjectFieldStart("@context");
// do not repeat vocab if already defined in current context
if (parentContext == null || parentContext.vocab == null ||
(currentContext.vocab != null && !currentContext.vocab.equals(parentContext.vocab))) {
jgen.writeStringField(JsonLdKeywords.AT_VOCAB, currentContext.vocab);
}
for (Map.Entry<String, Object> termEntry : currentContext.terms.entrySet()) {
if (termEntry.getValue() instanceof String) {
jgen.writeStringField(termEntry.getKey(), termEntry.getValue()
.toString());
} else {
jgen.writeObjectField(termEntry.getKey(), termEntry.getValue());
}
}
jgen.writeEndObject();
// end context
}
}
@Override
public JsonSerializer<Object> unwrappingSerializer(NameTransformer unwrapper) {
UnwrappingJacksonHydraSerializer unwrappingJacksonHydraSerializer = new UnwrappingJacksonHydraSerializer
(this, proxyUnwrapper);
return unwrappingJacksonHydraSerializer;
}
@Override
public void resolve(SerializerProvider provider) throws JsonMappingException {
super.resolve(provider);
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider provider,
BeanProperty property) throws JsonMappingException {
return super.createContextual(provider, property);
}
}