package de.escalon.hypermedia.hydra.serialize; import de.escalon.hypermedia.AnnotationUtils; import de.escalon.hypermedia.hydra.mapping.*; import org.apache.commons.lang3.text.WordUtils; import java.beans.BeanInfo; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; import static de.escalon.hypermedia.AnnotationUtils.findAnnotation; /** * Provides LdContext information. Created by Dietrich on 05.04.2015. */ public class LdContextFactory { public static final String HTTP_SCHEMA_ORG = "http://schema.org/"; private ProxyUnwrapper proxyUnwrapper; /** * Gets vocab for given bean. * * @param bean * to inspect for vocab * @param mixInClass * for bean which might define a vocab or has a context provider * @return explicitly defined vocab or http://schema.org */ public String getVocab(MixinSource mixinSource, Object bean, Class<?> mixInClass) { if (proxyUnwrapper != null) { bean = proxyUnwrapper.unwrapProxy(bean); } // determine vocab in context String classVocab = bean == null ? null : vocabFromClassOrPackage(bean.getClass()); final Vocab mixinVocab = findAnnotation(mixInClass, Vocab.class); Object nestedContextProviderFromMixin = getNestedContextProviderFromMixin(mixinSource, bean, mixInClass); String contextProviderVocab = null; if (nestedContextProviderFromMixin != null) { contextProviderVocab = getVocab(mixinSource, nestedContextProviderFromMixin, null); } String vocab; if (mixinVocab != null) { vocab = mixinVocab.value(); // wins over class } else if (classVocab != null) { vocab = classVocab; // wins over context provider } else if (contextProviderVocab != null) { vocab = contextProviderVocab; // wins over last resort } else { vocab = HTTP_SCHEMA_ORG; } return vocab; } public Map<String, Object> getTerms(MixinSource mixinSource, Object bean, Class<?> mixInClass) { try { if (proxyUnwrapper != null) { bean = proxyUnwrapper.unwrapProxy(bean); } Map<String, Object> termsMap = new LinkedHashMap<String, Object>(); if (bean != null) { final Class<?> beanClass = bean.getClass(); termsMap.putAll(termsFromClass(beanClass)); Map<String, Object> mixinTermsMap = getAnnotatedTerms(mixInClass, beanClass .getName()); // mixin terms override class terms termsMap.putAll(mixinTermsMap); Object nestedContextProviderFromMixin = getNestedContextProviderFromMixin(mixinSource, bean, mixInClass); if (nestedContextProviderFromMixin != null) { termsMap.putAll(getTerms(mixinSource, nestedContextProviderFromMixin, null)); } final Field[] fields = beanClass .getDeclaredFields(); for (Field field : fields) { if (Modifier.isPublic(field.getModifiers())) { final Expose expose = field.getAnnotation(Expose.class); if (Enum.class.isAssignableFrom(field.getType())) { addEnumTerms(termsMap, expose, field.getName(), (Enum) field.get(bean)); } else { if (expose != null) { termsMap.put(field.getName(), expose.value()); } } } } final BeanInfo beanInfo = Introspector.getBeanInfo(beanClass); final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { final Method method = propertyDescriptor.getReadMethod(); if (method != null) { final Expose expose = method.getAnnotation(Expose.class); if (Enum.class.isAssignableFrom(method.getReturnType())) { addEnumTerms(termsMap, expose, propertyDescriptor.getName(), (Enum) method.invoke(bean)); } else { if (expose != null) { termsMap.put(propertyDescriptor.getName(), expose.value()); } } } } } return termsMap; } catch (Exception e) { throw new RuntimeException(e); } } /** * Gets explicitly defined terms, e.g. on package, class or mixin. * * @param annotatedElement * to find terms * @param name * of annotated element, i.e. class name or package name * @return terms */ private Map<String, Object> getAnnotatedTerms(AnnotatedElement annotatedElement, String name) { final Terms annotatedTerms = findAnnotation(annotatedElement, Terms.class); final Term annotatedTerm = findAnnotation(annotatedElement, Term.class); if (annotatedTerms != null && annotatedTerm != null) { throw new IllegalStateException("found both @Terms and @Term in " + name + ", use either one or the other"); } Map<String, Object> annotatedTermsMap = new LinkedHashMap<String, Object>(); if (annotatedTerms != null) { final Term[] terms = annotatedTerms.value(); for (Term term : terms) { collectTerms(name, annotatedTermsMap, term); } } else if (annotatedTerm != null) { // only one term collectTerms(name, annotatedTermsMap, annotatedTerm); } return annotatedTermsMap; } private void collectTerms(String name, Map<String, Object> annotatedTermsMap, Term term) { final String define = term.define(); final String as = term.as(); final boolean reverse = term.reverse(); if (annotatedTermsMap.containsKey(as)) { throw new IllegalStateException("duplicate definition of term '" + define + "' in " + name); } if (!reverse) { annotatedTermsMap.put(define, as); } else { Map<String, String> reverseTerm = new LinkedHashMap<String, String>(); reverseTerm.put(JsonLdKeywords.AT_REVERSE, as); annotatedTermsMap.put(define, reverseTerm); } } private Object getNestedContextProviderFromMixin(MixinSource mixinSource, Object bean, Class<?> mixinClass) { // TODO does not consider Collection<Resource> or Collection<PersistentEntityResource> to find mixin of // object wrapped in resource // TODO does not consider package of object wrapped in resource // TODO: we do not know Resources here if (mixinClass == null) { return null; } try { Method mixinContextProvider = getContextProvider(mixinClass); if (mixinContextProvider == null) { return null; } Class<?> beanClass = bean.getClass(); Object contextual = beanClass.getMethod(mixinContextProvider.getName()) .invoke(bean); Object ret = null; if (contextual instanceof Collection) { Collection collection = (Collection) contextual; if (!collection.isEmpty()) { Object item = collection.iterator() .next(); final Class<?> mixInClass = mixinSource.findMixInClassFor(item.getClass()); if (mixInClass == null) { ret = item; } else { ret = getNestedContextProviderFromMixin(mixinSource, item, mixInClass); } } } else if (contextual instanceof Map) { Map map = (Map) contextual; if (!map.isEmpty()) { Object item = map.values() .iterator() .next(); final Class<?> mixInClass = mixinSource.findMixInClassFor(item.getClass()); if (mixInClass == null) { ret = item; } else { ret = getNestedContextProviderFromMixin(mixinSource, item, mixInClass); } } } else { ret = contextual; } return ret; } catch (Exception e) { throw new RuntimeException(e); } } private Method getContextProvider(Class<?> beanClass) { Class<? extends Annotation> annotation = ContextProvider.class; Method contextProvider = AnnotationUtils.getAnnotatedMethod(beanClass, annotation); if (contextProvider != null && contextProvider.getParameterTypes().length > 0) { throw new IllegalStateException("the context provider method " + contextProvider.getName() + " must not " + "have arguments"); } return contextProvider; } private void addEnumTerms(Map<String, Object> termsMap, Expose expose, String name, Enum value) throws NoSuchFieldException { if (value != null) { Map<String, String> map = new LinkedHashMap<String, String>(); if (expose != null) { map.put(JsonLdKeywords.AT_ID, expose.value()); } map.put(JsonLdKeywords.AT_TYPE, JsonLdKeywords.AT_VOCAB); termsMap.put(name, map); final Expose enumValueExpose = findAnnotation(value.getClass() .getField(value.name()), Expose.class); if (enumValueExpose != null) { termsMap.put(value.toString(), enumValueExpose.value()); } else { // might use upperToCamelCase if nothing is exposed final String camelCaseEnumValue = WordUtils.capitalizeFully(value.toString(), new char[]{'_'}) .replaceAll("_", ""); termsMap.put(value.toString(), camelCaseEnumValue); } } } public String vocabFromClassOrPackage(Class<?> clazz) { // vocab and terms of defining class: class and package final Vocab packageVocab = findAnnotation(clazz .getPackage(), Vocab.class); final Vocab classVocab = findAnnotation(clazz, Vocab.class); String vocab; if (classVocab != null) { vocab = classVocab.value(); // wins over package } else if (packageVocab != null) { vocab = packageVocab.value(); // wins over context provider } else { vocab = null; } return vocab; } public Map<String, Object> termsFromClass(Class<?> clazz) { Map<String, Object> termsMap = getAnnotatedTerms(clazz.getPackage(), clazz.getPackage() .getName()); Map<String, Object> classTermsMap = getAnnotatedTerms(clazz, clazz.getName()); // class terms override package terms termsMap.putAll(classTermsMap); return termsMap; } public void setProxyUnwrapper(ProxyUnwrapper proxyUnwrapper) { this.proxyUnwrapper = proxyUnwrapper; } }