From 036176f587d5492d08431fdb212b4650b288eb0c Mon Sep 17 00:00:00 2001 From: Thomas Seidel Date: Thu, 11 Dec 2014 18:29:57 +0100 Subject: [PATCH 1/3] Add `PojoDecoder` Outputs a record by decoding the members of a given pojo (Plain Old Java Object). Each public getter or public field defines a member. The member name is either the getter name without the get prefix or the field name. --- .../mf/stream/converter/PojoDecoder.java | 440 ++++++++++++++++++ .../mf/stream/converter/PojoDecoderTest.java | 334 +++++++++++++ 2 files changed, 774 insertions(+) create mode 100644 src/main/java/org/culturegraph/mf/stream/converter/PojoDecoder.java create mode 100644 src/test/java/org/culturegraph/mf/stream/converter/PojoDecoderTest.java diff --git a/src/main/java/org/culturegraph/mf/stream/converter/PojoDecoder.java b/src/main/java/org/culturegraph/mf/stream/converter/PojoDecoder.java new file mode 100644 index 000000000..8b53e0f6f --- /dev/null +++ b/src/main/java/org/culturegraph/mf/stream/converter/PojoDecoder.java @@ -0,0 +1,440 @@ +/* + * Copyright 2013, 2014 Deutsche Nationalbibliothek + * + * 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 org.culturegraph.mf.stream.converter; + +import java.beans.Introspector; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.culturegraph.mf.exceptions.MetafactureException; +import org.culturegraph.mf.framework.DefaultObjectPipe; +import org.culturegraph.mf.framework.StreamReceiver; +import org.culturegraph.mf.framework.annotations.Description; +import org.culturegraph.mf.framework.annotations.In; +import org.culturegraph.mf.framework.annotations.Out; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Outputs a record by decoding the members of a given pojo (Plain Old Java + * Object). Each public getter or public field defines a member. The member name + * is either the getter name without the get prefix or the field name. + * + *

+ * The generated stream events depend on the member type: + *

+ *

+ * + *

+ * Here are some examples: + *

+ *

+ * + *

+ * See {@link PojoDecoderTest} for more examples. + *

+ * + * @param + * input object type + * @author Thomas Seidel + * + */ +@Description("Outputs a record containing the member values of the input pojo (Plain Old Java Object)") +@In(Object.class) +@Out(StreamReceiver.class) +public class PojoDecoder extends DefaultObjectPipe { + + private static final Logger LOG = LoggerFactory + .getLogger(PojoDecoder.class); + + /** + * Use this interfaces to include a metafacture event stream to the pojo + * decoder. If the {@link PojoDecoder} detects a type implementing this + * interface, it will call the {@link #sendToStream} method. + * + * @author Thomas Seidel + * + */ + public interface MetafactureSource { + void sendToStream(final StreamReceiver streamReceiver); + } + + /** + * A ValueGetter retrieves a pojos's member, via getter method or field + * access. Used by {@link ComplexTypeDecoder} only. + * + * @author Thomas Seidel + * + */ + private interface ValueGetter { + + Object getValue(final Object object); + + String getName(); + + Class getValueType(); + + } + + private static class MethodValueGetter implements ValueGetter { + + private static final String METHOD_PREFIX = "get"; + + private final String name; + private final Method method; + + public static boolean supportsMethod(final Method m) { + return Modifier.isPublic(m.getModifiers()) + && m.getName().length() > METHOD_PREFIX.length() + && m.getName().startsWith(METHOD_PREFIX); + } + + public MethodValueGetter(final Method method) { + assert supportsMethod(method); + this.method = method; + // remove prefix then lower case first character + name = Introspector.decapitalize(method.getName().substring( + METHOD_PREFIX.length())); + } + + @Override + public Object getValue(final Object object) { + try { + return method.invoke(object); + } catch (final IllegalArgumentException e) { + throw new MetafactureException( + "The given object don't have a method named " + + method.getName(), e); + } catch (final IllegalAccessException e) { + throw new MetafactureException("Can't access the method named " + + method.getName(), e); + } catch (final InvocationTargetException e) { + throw new MetafactureException("Invoking the method named " + + method.getName() + " throws an excpetion", e); + } + } + + @Override + public String getName() { + return name; + } + + @Override + public Class getValueType() { + return method.getReturnType(); + } + + } + + private static class FieldValueGetter implements ValueGetter { + + final Field field; + + public static boolean supportsField(final Field f) { + return Modifier.isPublic(f.getModifiers()); + } + + public FieldValueGetter(final Field field) { + assert supportsField(field); + this.field = field; + } + + @Override + public Object getValue(final Object object) { + try { + return field.get(object); + } catch (final IllegalArgumentException e) { + throw new MetafactureException( + "The given object don't have a field named " + + field.getName(), e); + } catch (final IllegalAccessException e) { + throw new MetafactureException("Can't access the field named " + + field.getName(), e); + } + } + + @Override + public String getName() { + return field.getName(); + } + + @Override + public Class getValueType() { + return field.getType(); + } + + } + + /** + * A TypeDecoder decodes an object to a metafacture stream using a given + * name. + * + * @author Thomas Seidel + * + */ + private interface TypeDecoder { + + void decodeToStream(final StreamReceiver streamReceiver, + final String name, final Object object); + + } + + private final TypeDecoderFactory typeDecoderFactory = new TypeDecoderFactory(); + + private static class TypeDecoderFactory { + private final Map, TypeDecoder> typeDecoders = new HashMap, TypeDecoder>(); + + private TypeDecoder create(final Class clazz) { + if (typeDecoders.containsKey(clazz)) { + return typeDecoders.get(clazz); + } + TypeDecoder typeDecoder; + if (SimpleTypeDecoder.supportsType(clazz)) { + typeDecoder = new SimpleTypeDecoder(); + } else if (MetafactureSourceTypeDecoder.supportsType(clazz)) { + typeDecoder = new MetafactureSourceTypeDecoder(); + } else if (CollectionTypeDecoder.supportsType(clazz)) { + typeDecoder = new CollectionTypeDecoder(this); + } else if (ArrayTypeDecoder.supportsType(clazz)) { + typeDecoder = new ArrayTypeDecoder(this); + } else if (ComplexTypeDecoder.supportsType(clazz)) { + typeDecoder = new ComplexTypeDecoder(clazz, this); + } else if (MapTypeDecoder.supportsType(clazz)) { + typeDecoder = new MapTypeDecoder(this); + } else { + throw new MetafactureException("Can't decode type " + clazz); + } + typeDecoders.put(clazz, typeDecoder); + LOG.debug("typeDecoders: {})", typeDecoders); + return typeDecoder; + } + } + + private static class SimpleTypeDecoder implements TypeDecoder { + + public static boolean supportsType(final Class clazz) { + return clazz.isPrimitive() || clazz.equals(String.class); + } + + @Override + public void decodeToStream(final StreamReceiver streamReceiver, + final String name, final Object object) { + streamReceiver.literal(name, object.toString()); + } + + } + + private static class MetafactureSourceTypeDecoder implements TypeDecoder { + + public static boolean supportsType(final Class clazz) { + return MetafactureSource.class.isAssignableFrom(clazz); + } + + @Override + public void decodeToStream(final StreamReceiver streamReceiver, + final String name, final Object object) { + final MetafactureSource metafactureSource = (MetafactureSource) object; + streamReceiver.startEntity(name); + metafactureSource.sendToStream(streamReceiver); + streamReceiver.endEntity(); + } + + } + + private static class ComplexTypeDecoder implements TypeDecoder { + + private final TypeDecoderFactory typeDecoderFactury; + private final List valueGetters; + + public static boolean supportsType(final Class clazz) { + return !SimpleTypeDecoder.supportsType(clazz) + && !MetafactureSourceTypeDecoder.supportsType(clazz) + && !CollectionTypeDecoder.supportsType(clazz) + && !ArrayTypeDecoder.supportsType(clazz) + && !MapTypeDecoder.supportsType(clazz); + } + + public ComplexTypeDecoder(final Class clazz, + final TypeDecoderFactory typeDecoderFactury) { + this.typeDecoderFactury = typeDecoderFactury; + valueGetters = new ArrayList(); + // get all public fields of this class and all super classes + final Field[] fields = clazz.getDeclaredFields(); + for (final Field field : fields) { + if (FieldValueGetter.supportsField(field)) { + valueGetters.add(new FieldValueGetter(field)); + } + } + // get all valid public methods of this class and all super classes + final Method[] methods = clazz.getDeclaredMethods(); + for (final Method method : methods) { + if (MethodValueGetter.supportsMethod(method)) { + valueGetters.add(new MethodValueGetter(method)); + } + } + } + + @Override + public void decodeToStream(final StreamReceiver streamReceiver, + final String name, final Object object) { + + if (name != null) { + streamReceiver.startEntity(name); + } + for (final ValueGetter valueGetter : valueGetters) { + final Object value = valueGetter.getValue(object); + final Class valueType = valueGetter.getValueType(); + final String valueName = valueGetter.getName(); + final TypeDecoder typeDecoder = typeDecoderFactury + .create(valueType); + typeDecoder.decodeToStream(streamReceiver, valueName, value); + } + if (name != null) { + streamReceiver.endEntity(); + } + } + } + + private static class CollectionTypeDecoder implements TypeDecoder { + + private final TypeDecoderFactory typeDecoderFactury; + + public CollectionTypeDecoder(final TypeDecoderFactory typeDecoderFactury) { + this.typeDecoderFactury = typeDecoderFactury; + } + + public static boolean supportsType(final Class clazz) { + return Collection.class.isAssignableFrom(clazz); + } + + @Override + public void decodeToStream(final StreamReceiver streamReceiver, + final String name, final Object object) { + final Collection collection = (Collection) object; + for (final Object element : collection) { + final TypeDecoder typeDecoder = typeDecoderFactury + .create(element.getClass()); + typeDecoder.decodeToStream(streamReceiver, name, element); + } + } + } + + private static class ArrayTypeDecoder implements TypeDecoder { + + private final TypeDecoderFactory typeDecoderFactury; + + public ArrayTypeDecoder(final TypeDecoderFactory typeDecoderFactury) { + this.typeDecoderFactury = typeDecoderFactury; + } + + public static boolean supportsType(final Class clazz) { + return clazz.isArray(); + } + + @Override + public void decodeToStream(final StreamReceiver streamReceiver, + final String name, final Object object) { + final Object[] array = (Object[]) object; + for (final Object element : array) { + final TypeDecoder typeDecoder = typeDecoderFactury + .create(element.getClass()); + typeDecoder.decodeToStream(streamReceiver, name, element); + } + } + + } + + private static class MapTypeDecoder implements TypeDecoder { + + private final TypeDecoderFactory typeDecoderFactury; + + public MapTypeDecoder(final TypeDecoderFactory typeDecoderFactury) { + this.typeDecoderFactury = typeDecoderFactury; + } + + public static boolean supportsType(final Class clazz) { + return Map.class.isAssignableFrom(clazz); + } + + @Override + public void decodeToStream(final StreamReceiver streamReceiver, + final String name, final Object object) { + final Map map = (Map) object; + if (name != null) { + streamReceiver.startEntity(name); + } + for (final Entry entry : map.entrySet()) { + final String key = entry.getKey().toString(); + final Object value = entry.getValue(); + final TypeDecoder typeDecoder = typeDecoderFactury.create(value + .getClass()); + typeDecoder.decodeToStream(streamReceiver, key, value); + } + if (name != null) { + streamReceiver.endEntity(); + } + } + } + + @Override + public void process(final T obj) { + if (obj == null) { + return; + } + assert !isClosed(); + final TypeDecoder typeDecoder = typeDecoderFactory.create(obj + .getClass()); + getReceiver().startRecord(""); + typeDecoder.decodeToStream(getReceiver(), null, obj); + getReceiver().endRecord(); + } + +} diff --git a/src/test/java/org/culturegraph/mf/stream/converter/PojoDecoderTest.java b/src/test/java/org/culturegraph/mf/stream/converter/PojoDecoderTest.java new file mode 100644 index 000000000..2f9693caf --- /dev/null +++ b/src/test/java/org/culturegraph/mf/stream/converter/PojoDecoderTest.java @@ -0,0 +1,334 @@ +/* + * Copyright 2013, 2014 Deutsche Nationalbibliothek + * + * 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 org.culturegraph.mf.stream.converter; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.culturegraph.mf.framework.StreamReceiver; +import org.culturegraph.mf.stream.converter.PojoDecoder.MetafactureSource; +import org.culturegraph.mf.stream.sink.EventList; +import org.culturegraph.mf.stream.sink.StreamValidator; +import org.junit.Test; + +/** + * Tests for {@link PojoDecoder} + * + * @author Thomas Seidel + * + */ +public class PojoDecoderTest { + + private final static String firstFieldName = "firstField"; + private final static String secondFieldName = "secondField"; + private final static String firstFieldValue = "firstValue"; + private final static String secondFieldValue = "secondValue"; + + private final static String innerPojoName = "innerPojo"; + + private final static String metafactureSourceField = "metafactureSourceField"; + private final static String metafactureSourceName = "metafactureSourceName"; + private final static String metafactureSourceValue = "metafactureSourceValue"; + + private final static String listFieldName = "listField"; + private final static String firstListFieldValue = "firstListFieldValue"; + private final static String secondListFieldValue = "secondListFieldValue"; + private final static String setFieldName = "setField"; + private final static String firstSetFieldValue = "firstSetFieldValue"; + private final static String secondSetFieldValue = "secondSetFieldValue"; + private final static String arrayFieldName = "arrayField"; + private final static String firstArrayFieldValue = "firstArrayFieldValue"; + private final static String secondArrayFieldValue = "secondArrayFieldValue"; + + private final static String mapFieldName = "mapField"; + private final static String firstMapFieldKey = "fistMapFieldKey"; + private final static String firstdMapFieldValue = "fistMapFieldValue"; + private final static String secondMapFieldKey = "secondMapFieldKey"; + private final static String secondMapFieldValue = "secondMapFieldKey"; + + private static class EmptyPojo { + } + + // Suppress warnings for unused fields or getters. The ObjectDecoder uses + // them to access the values. + private static class SimplePojo { + private String firstField; + @SuppressWarnings("unused") + public String secondField; + + @SuppressWarnings("unused") + public String getFirstField() { + return firstField; + } + + public void setFirstField(final String firstField) { + this.firstField = firstField; + } + + } + + // Suppress warnings for unused fields or getters. The ObjectDecoder uses + // them to access the values. + private static class NestedPojo { + private SimplePojo innerPojo; + + @SuppressWarnings("unused") + public SimplePojo getInnerPojo() { + return innerPojo; + } + + public final void setInnerPojo(final SimplePojo innerPojo) { + this.innerPojo = innerPojo; + } + } + + // Suppress warnings for unused fields or getters. The ObjectDecoder uses + // them to access the values. + private static class NestedMetafactureSourcePojo { + private MetafactureSource metafactureSourceField; + + @SuppressWarnings("unused") + public MetafactureSource getMetafactureSourceField() { + return metafactureSourceField; + } + + public void setMetafactureSourceField( + final MetafactureSource metafactureSourceField) { + this.metafactureSourceField = metafactureSourceField; + } + + } + + // Suppress warnings for unused fields or getters. The ObjectDecoder uses + // them to access the values. + private static class SimpleCollectionAndArrayPojo { + private List listField; + private Set setField; + private String[] arrayField; + private String firstField; + + @SuppressWarnings("unused") + public List getListField() { + return listField; + } + + public void setListField(final List listField) { + this.listField = listField; + } + + @SuppressWarnings("unused") + public Set getSetField() { + return setField; + } + + public void setSetField(final Set setField) { + this.setField = setField; + } + + @SuppressWarnings("unused") + public String[] getArrayField() { + return arrayField; + } + + public void setArrayField(final String[] arrayField) { + this.arrayField = arrayField; + } + + @SuppressWarnings("unused") + public String getFirstField() { + return firstField; + } + + public void setFirstField(final String firstField) { + this.firstField = firstField; + } + + } + + private static class SimpleMapPojo { + private Map mapField; + + @SuppressWarnings("unused") + public Map getMapField() { + return mapField; + } + + public void setMapField(final Map mapField) { + this.mapField = mapField; + } + } + + @Test + public void shouldDecodeNullObject() { + // create validator + final EventList expected = new EventList(); + final StreamValidator validator = new StreamValidator( + expected.getEvents()); + // decode null pojo and verify result + final PojoDecoder pojoDecoder = new PojoDecoder(); + pojoDecoder.setReceiver(validator); + pojoDecoder.process(null); + } + + @Test + public void shouldDecodeEmptyPojo() { + // create pojo + final EmptyPojo emptyPojo = new EmptyPojo(); + // create validator + final EventList expected = new EventList(); + expected.startRecord(""); + expected.endRecord(); + final StreamValidator validator = new StreamValidator( + expected.getEvents()); + // decode pojo and verify result + final PojoDecoder pojoDecoder = new PojoDecoder(); + pojoDecoder.setReceiver(validator); + pojoDecoder.process(emptyPojo); + } + + @Test + public void shouldDecodeSimplePojo() { + // create pojo + final SimplePojo simplePojo = new SimplePojo(); + simplePojo.setFirstField(firstFieldValue); + simplePojo.secondField = secondFieldValue; + // create validator + final EventList expected = new EventList(); + expected.startRecord(""); + expected.literal(firstFieldName, firstFieldValue); + expected.literal(secondFieldName, secondFieldValue); + expected.endRecord(); + final StreamValidator validator = new StreamValidator( + expected.getEvents()); + // decode pojo and verify result + final PojoDecoder pojoDecoder = new PojoDecoder(); + pojoDecoder.setReceiver(validator); + pojoDecoder.process(simplePojo); + } + + @Test + public void shouldDecodeNestedPojo() { + // create pojo + final SimplePojo simplePojo = new SimplePojo(); + simplePojo.setFirstField(firstFieldValue); + simplePojo.secondField = secondFieldValue; + final NestedPojo nestedPojo = new NestedPojo(); + nestedPojo.setInnerPojo(simplePojo); + // create validator + final EventList expected = new EventList(); + expected.startRecord(""); + expected.startEntity(innerPojoName); + expected.literal(firstFieldName, firstFieldValue); + expected.literal(secondFieldName, secondFieldValue); + expected.endEntity(); + expected.endRecord(); + final StreamValidator validator = new StreamValidator( + expected.getEvents()); + // decode pojo and verify result + final PojoDecoder pojoDecoder = new PojoDecoder(); + pojoDecoder.setReceiver(validator); + pojoDecoder.process(nestedPojo); + } + + @Test + public void shouldDecodePojoWithMetafactureSource() { + // create pojo + final MetafactureSource metafactureSource = new MetafactureSource() { + @Override + public void sendToStream(final StreamReceiver streamReceiver) { + streamReceiver.literal(metafactureSourceName, + metafactureSourceValue); + } + }; + final NestedMetafactureSourcePojo nestedMetafactureSourcePojo = new NestedMetafactureSourcePojo(); + nestedMetafactureSourcePojo + .setMetafactureSourceField(metafactureSource); + // create validator + final EventList expected = new EventList(); + expected.startRecord(""); + expected.startEntity(metafactureSourceField); + expected.literal(metafactureSourceName, metafactureSourceValue); + expected.endEntity(); + expected.endRecord(); + final StreamValidator validator = new StreamValidator( + expected.getEvents()); + // decode pojo and verify result + final PojoDecoder pojoDecoder = new PojoDecoder(); + pojoDecoder.setReceiver(validator); + pojoDecoder.process(nestedMetafactureSourcePojo); + } + + @Test + public void shouldDecodeSimpleCollectionAndArrayPojo() { + // create pojo + final List listField = Arrays.asList(firstListFieldValue, + secondListFieldValue); + final Set setField = new HashSet(Arrays.asList( + firstSetFieldValue, secondSetFieldValue)); + final String[] arrayField = { firstArrayFieldValue, + secondArrayFieldValue }; + final SimpleCollectionAndArrayPojo simpleCollectionAndArrayPojo = new SimpleCollectionAndArrayPojo(); + simpleCollectionAndArrayPojo.setListField(listField); + simpleCollectionAndArrayPojo.setSetField(setField); + simpleCollectionAndArrayPojo.setArrayField(arrayField); + simpleCollectionAndArrayPojo.setFirstField(firstFieldValue); + // create validator + final EventList expected = new EventList(); + expected.startRecord(""); + expected.literal(listFieldName, firstListFieldValue); + expected.literal(listFieldName, secondListFieldValue); + expected.literal(setFieldName, firstSetFieldValue); + expected.literal(setFieldName, secondSetFieldValue); + expected.literal(arrayFieldName, firstArrayFieldValue); + expected.literal(arrayFieldName, secondArrayFieldValue); + expected.literal(firstFieldName, firstFieldValue); + expected.endRecord(); + final StreamValidator validator = new StreamValidator( + expected.getEvents()); + // decode pojo and verify result + final PojoDecoder pojoDecoder = new PojoDecoder(); + pojoDecoder.setReceiver(validator); + pojoDecoder.process(simpleCollectionAndArrayPojo); + } + + @Test + public void shouldDecodeSimpleMapPojo() { + // create pojo + final Map mapField = new HashMap(); + mapField.put(firstMapFieldKey, firstdMapFieldValue); + mapField.put(secondMapFieldKey, secondMapFieldValue); + final SimpleMapPojo simpleMapPojo = new SimpleMapPojo(); + simpleMapPojo.setMapField(mapField); + // create validator + final EventList expected = new EventList(); + expected.startRecord(""); + expected.startEntity(mapFieldName); + expected.literal(firstMapFieldKey, firstdMapFieldValue); + expected.literal(secondMapFieldKey, secondMapFieldValue); + expected.endEntity(); + expected.endRecord(); + final StreamValidator validator = new StreamValidator( + expected.getEvents()); + // decode pojo and verify result + final PojoDecoder pojoDecoder = new PojoDecoder(); + pojoDecoder.setReceiver(validator); + pojoDecoder.process(simpleMapPojo); + } +} From 948e217ce7c1e95d272afcd0261aa00d9e1c6feb Mon Sep 17 00:00:00 2001 From: Thomas Seidel Date: Thu, 11 Dec 2014 18:30:40 +0100 Subject: [PATCH 2/3] Add `PojoEncoder` This class creates and fills a new object instance with stream data and sends the result to the given object receiver. --- .../mf/stream/converter/PojoEncoder.java | 393 ++++++++++++++++++ .../mf/stream/converter/PojoEncoderTest.java | 198 +++++++++ 2 files changed, 591 insertions(+) create mode 100644 src/main/java/org/culturegraph/mf/stream/converter/PojoEncoder.java create mode 100644 src/test/java/org/culturegraph/mf/stream/converter/PojoEncoderTest.java diff --git a/src/main/java/org/culturegraph/mf/stream/converter/PojoEncoder.java b/src/main/java/org/culturegraph/mf/stream/converter/PojoEncoder.java new file mode 100644 index 000000000..0f7ab6e64 --- /dev/null +++ b/src/main/java/org/culturegraph/mf/stream/converter/PojoEncoder.java @@ -0,0 +1,393 @@ +package org.culturegraph.mf.stream.converter; + +import java.beans.Introspector; +import java.beans.PropertyEditor; +import java.beans.PropertyEditorManager; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.culturegraph.mf.exceptions.MetafactureException; +import org.culturegraph.mf.framework.DefaultStreamPipe; +import org.culturegraph.mf.framework.ObjectReceiver; +import org.culturegraph.mf.framework.StreamReceiver; +import org.culturegraph.mf.framework.annotations.Description; +import org.culturegraph.mf.framework.annotations.In; +import org.culturegraph.mf.framework.annotations.Out; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class creates and fills a new object instance with stream data and sends + * the result to the given object receiver. + * + * @author Thomas Seidel + * + * @param + * The type of the object to create. + * + */ +@Description("Creates a pojo (Plain Old Java Object) based on a record containing the member values") +@In(StreamReceiver.class) +@Out(Object.class) +public class PojoEncoder extends DefaultStreamPipe> { + + private static final Logger LOG = LoggerFactory + .getLogger(PojoEncoder.class); + + private static class ValueType { + private final Class rawClass; + private Class elementClass; + + public ValueType(final Class clazz) { + rawClass = clazz; + } + + public ValueType(final Class clazz, final Type type) { + rawClass = clazz; + if (type instanceof ParameterizedType) { + elementClass = (Class) (((ParameterizedType) type) + .getActualTypeArguments()[0]); + } + } + + public Class getRawClass() { + return rawClass; + } + + public Class getElementClass() { + return elementClass; + } + + } + + /** + * A ValueSetter sets a pojos's member, via setter method or field access. + * Used by {@link ComplexTypeEncoder} only + * + * @author Thomas Seidel + * + */ + private interface ValueSetter { + + void setValue(final Object object, final Object value); + + String getName(); + + ValueType getValueType(); + + } + + private static class MethodValueSetter implements ValueSetter { + + private static final String METHOD_PREFIX = "set"; + + private final String name; + private final Method method; + + public static boolean supportsMethod(final Method m) { + return Modifier.isPublic(m.getModifiers()) + && m.getName().length() > METHOD_PREFIX.length() + && m.getName().startsWith(METHOD_PREFIX) + && m.getParameterTypes().length == 1; + } + + public MethodValueSetter(final Method method) { + assert supportsMethod(method); + this.method = method; + // remove prefix then lower case first character + name = Introspector.decapitalize(method.getName().substring( + METHOD_PREFIX.length())); + } + + @Override + public void setValue(final Object object, final Object value) { + try { + method.invoke(object, value); + } catch (final IllegalArgumentException e) { + throw new MetafactureException( + "The given object don't have a method named " + + method.getName(), e); + } catch (final IllegalAccessException e) { + throw new MetafactureException("Can't access the method named " + + method.getName(), e); + } catch (final InvocationTargetException e) { + throw new MetafactureException("Invoking the method named " + + method.getName() + " throws an excpetion", e); + } + } + + @Override + public String getName() { + return name; + } + + @Override + public ValueType getValueType() { + return new ValueType(method.getParameterTypes()[0], + method.getGenericParameterTypes()[0]); + } + + } + + private static class FieldValueSetter implements ValueSetter { + + final Field field; + + public static boolean supportsField(final Field f) { + return Modifier.isPublic(f.getModifiers()); + } + + public FieldValueSetter(final Field field) { + assert supportsField(field); + this.field = field; + } + + @Override + public void setValue(final Object object, final Object value) { + try { + field.set(object, value); + } catch (final IllegalArgumentException e) { + throw new MetafactureException( + "The given object don't have a field named " + + field.getName(), e); + } catch (final IllegalAccessException e) { + throw new MetafactureException("Can't access the field named " + + field.getName(), e); + } + } + + @Override + public String getName() { + return field.getName(); + } + + @Override + public ValueType getValueType() { + return new ValueType(field.getType(), field.getGenericType()); + } + + } + + /** + * A TypeEncoder encodes a metafacture stream to a new object + * + * @author Thomas Seidel + * + */ + private interface TypeEncoder { + + void setValue(String name, Object value); + + ValueType getValueType(String name); + + Object getInstance(); + + } + + private final TypeEncoderFactory typeEncoderFactory = new TypeEncoderFactory(); + + private static class TypeEncoderFactory { + private final Map, TypeEncoder> typeEncoders = new HashMap, TypeEncoder>(); + + private TypeEncoder create(final ValueType valueType) { + final TypeEncoder typeEncoder; + // if (typeEncoders.containsKey(clazz)) { + // return typeEncoders.get(clazz); + // } + final Class rawClass = valueType.getRawClass(); + if (ListTypeEncoder.supportsType(rawClass)) { + typeEncoder = new ListTypeEncoder(valueType); + } else if (ComplexTypeEncoder.supportsType(rawClass)) { + typeEncoder = new ComplexTypeEncoder(rawClass); + } else { + throw new MetafactureException("Can't encode type " + rawClass); + } + typeEncoders.put(rawClass, typeEncoder); + LOG.debug("typeEncoders: {})", typeEncoders); + return typeEncoder; + } + } + + private static class ComplexTypeEncoder implements TypeEncoder { + + private final Object instance; + private final Map valueSetters; + + public ComplexTypeEncoder(final Class clazz) { + assert supportsType(clazz); + instance = createInstance(clazz); + valueSetters = new HashMap(); + // get all public fields of this class and all super classes + final Field[] fields = clazz.getDeclaredFields(); + for (final Field field : fields) { + if (FieldValueSetter.supportsField(field)) { + final FieldValueSetter fieldValueSetter = new FieldValueSetter( + field); + valueSetters.put(fieldValueSetter.getName(), + fieldValueSetter); + } + } + // get all valid public methods of this class and all super classes + final Method[] methods = clazz.getDeclaredMethods(); + for (final Method method : methods) { + if (MethodValueSetter.supportsMethod(method)) { + final MethodValueSetter methodValueSetter = new MethodValueSetter( + method); + valueSetters.put(methodValueSetter.getName(), + methodValueSetter); + } + } + } + + public static boolean supportsType(final Class clazz) { + return !clazz.isPrimitive() && !clazz.equals(String.class) + && !ListTypeEncoder.supportsType(clazz); + } + + @Override + public void setValue(final String name, final Object value) { + final ValueSetter valueSetter = valueSetters.get(name); + valueSetter.setValue(instance, value); + } + + @Override + public ValueType getValueType(final String name) { + final ValueSetter valueSetter = valueSetters.get(name); + return valueSetter.getValueType(); + } + + @Override + public Object getInstance() { + return instance; + } + + } + + private static class ListTypeEncoder implements TypeEncoder { + + private final ValueType valueType; + private final List objects; + + public ListTypeEncoder(final ValueType valueType) { + this.valueType = valueType; + objects = new ArrayList(); + } + + public static boolean supportsType(final Class clazz) { + return List.class.isAssignableFrom(clazz); + } + + @Override + public void setValue(final String name, final Object value) { + objects.add(value); + } + + @Override + public ValueType getValueType(final String name) { + return new ValueType(valueType.getElementClass()); + } + + @Override + public Object getInstance() { + return objects; + } + + } + + private static Object createObjectFromString(final String value, + final Class targetType) { + final PropertyEditor propertyEditor = PropertyEditorManager + .findEditor(targetType); + propertyEditor.setAsText(value); + return propertyEditor.getValue(); + } + + static { + // Initialize the property manager to map the primitive data types to + // the corresponding object based types, e.g. int to Integer + PropertyEditorManager.registerEditor(Boolean.class, + PropertyEditorManager.findEditor(boolean.class).getClass()); + PropertyEditorManager.registerEditor(Integer.class, + PropertyEditorManager.findEditor(int.class).getClass()); + PropertyEditorManager.registerEditor(Long.class, PropertyEditorManager + .findEditor(long.class).getClass()); + } + + private static Object createInstance(final Class clazz) { + Object object; + try { + object = clazz.newInstance(); + } catch (final Exception e) { + throw new MetafactureException( + "Can't instantiate object of class: " + clazz, e); + } + return object; + } + + private final Class pojoClass; + private final Deque typeEncoderStack; + + public PojoEncoder(final Class pojoClass) { + this.pojoClass = pojoClass; + typeEncoderStack = new ArrayDeque(); + } + + @Override + public void startRecord(final String identifier) { + typeEncoderStack.clear(); + typeEncoderStack.push(new ComplexTypeEncoder(pojoClass)); + } + + @Override + public void startEntity(final String name) { + final TypeEncoder currentTypeEncoder = typeEncoderStack.peek(); + final ValueType newType = currentTypeEncoder.getValueType(name); + final TypeEncoder newTypeEncoder = typeEncoderFactory.create(newType); + currentTypeEncoder.setValue(name, newTypeEncoder.getInstance()); + typeEncoderStack.push(newTypeEncoder); + } + + @Override + public void literal(final String name, final String value) { + final TypeEncoder currentTypeEncoder = typeEncoderStack.peek(); + final Class targetType = currentTypeEncoder.getValueType(name) + .getRawClass(); + currentTypeEncoder.setValue(name, + createObjectFromString(value, targetType)); + } + + @Override + public void endEntity() { + typeEncoderStack.pop(); + } + + @SuppressWarnings("unchecked") + @Override + public void endRecord() { + assert typeEncoderStack.size() == 1; + final ObjectReceiver objectReceiver = getReceiver(); + objectReceiver.process((T) typeEncoderStack.peek().getInstance()); + typeEncoderStack.clear(); + } + + @Override + public void onCloseStream() { + typeEncoderStack.clear(); + } + + @Override + public void onResetStream() { + typeEncoderStack.clear(); + } + +} diff --git a/src/test/java/org/culturegraph/mf/stream/converter/PojoEncoderTest.java b/src/test/java/org/culturegraph/mf/stream/converter/PojoEncoderTest.java new file mode 100644 index 000000000..f0ccb0d02 --- /dev/null +++ b/src/test/java/org/culturegraph/mf/stream/converter/PojoEncoderTest.java @@ -0,0 +1,198 @@ +package org.culturegraph.mf.stream.converter; + +import java.util.List; + +import org.culturegraph.mf.stream.pipe.ObjectBuffer; +import org.junit.Assert; +import org.junit.Test; + +public class PojoEncoderTest { + + public static class EmptyPojo { + + } + + @Test + public void shouldEncodeEmptyEntityStreamToEmptyPojo() { + final ObjectBuffer objectBuffer = new ObjectBuffer( + 1); + final PojoEncoder pojoEncoder = new PojoEncoder( + EmptyPojo.class); + pojoEncoder.setReceiver(objectBuffer); + pojoEncoder.startRecord("identifier"); + pojoEncoder.endRecord(); + Assert.assertNotNull(objectBuffer.pop()); + } + + public static class SimplePojo { + public String firstStringAttribute; + private String secondStringAttribute; + public int firstIntegerAttribute; + private int secondIntegerAttribute; + + public void setSecondStringAttribute(final String secondStringAttribute) { + this.secondStringAttribute = secondStringAttribute; + } + + public String getSecondStringAttribute() { + return secondStringAttribute; + } + + public int getSecondIntegerAttribute() { + return secondIntegerAttribute; + } + + public void setSecondIntegerAttribute(final int secondIntegerAttribute) { + this.secondIntegerAttribute = secondIntegerAttribute; + } + } + + @Test + public void shouldEncodeEntityStreamToSimplePojo() { + final ObjectBuffer objectBuffer = new ObjectBuffer(); + final PojoEncoder pojoEncoder = new PojoEncoder( + SimplePojo.class); + pojoEncoder.setReceiver(objectBuffer); + pojoEncoder.startRecord("identifier"); + pojoEncoder.literal("firstStringAttribute", "firstStringValue"); + pojoEncoder.literal("secondStringAttribute", "secondStringValue"); + pojoEncoder.literal("firstIntegerAttribute", "42"); + pojoEncoder.literal("secondIntegerAttribute", "23"); + pojoEncoder.endRecord(); + final SimplePojo simplePojo = objectBuffer.pop(); + Assert.assertNotNull(simplePojo); + Assert.assertEquals("firstStringValue", simplePojo.firstStringAttribute); + Assert.assertEquals("secondStringValue", + simplePojo.getSecondStringAttribute()); + Assert.assertEquals(42, simplePojo.firstIntegerAttribute); + Assert.assertEquals(23, simplePojo.getSecondIntegerAttribute()); + } + + public static class NestedPojo { + public String attribute; + public SimplePojo simplePojo; + } + + public static class DoubleNestedPojo { + public NestedPojo nestedPojo; + } + + @Test + public void shouldEncodeEntityStreamToDoubleNestedPojo() { + final ObjectBuffer objectBuffer = new ObjectBuffer(); + final PojoEncoder pojoEncoder = new PojoEncoder( + DoubleNestedPojo.class); + pojoEncoder.setReceiver(objectBuffer); + pojoEncoder.startRecord("identifier"); + pojoEncoder.startEntity("nestedPojo"); + pojoEncoder.startEntity("simplePojo"); + pojoEncoder.literal("firstStringAttribute", "firstStringValue"); + pojoEncoder.literal("secondStringAttribute", "secondStringValue"); + pojoEncoder.endEntity(); + pojoEncoder.literal("attribute", "value"); + pojoEncoder.endEntity(); + pojoEncoder.endRecord(); + final DoubleNestedPojo doubleNestedPojo = objectBuffer.pop(); + Assert.assertNotNull(doubleNestedPojo); + final NestedPojo nestedPojo = doubleNestedPojo.nestedPojo; + Assert.assertNotNull(nestedPojo); + Assert.assertEquals("value", nestedPojo.attribute); + final SimplePojo innerPojo = nestedPojo.simplePojo; + Assert.assertNotNull(innerPojo); + Assert.assertEquals("firstStringValue", innerPojo.firstStringAttribute); + Assert.assertEquals("secondStringValue", + innerPojo.getSecondStringAttribute()); + } + + public static class StringListPojo { + public List stringList; + public String attribute; + } + + @Test + public void shouldEncodeEntityStreamToPojoWithStringList() { + final ObjectBuffer objectBuffer = new ObjectBuffer(); + final PojoEncoder pojoEncoder = new PojoEncoder( + StringListPojo.class); + pojoEncoder.setReceiver(objectBuffer); + pojoEncoder.startRecord("identifier"); + pojoEncoder.startEntity("stringList"); + pojoEncoder.literal("firstElement", "firstValue"); + pojoEncoder.literal("secondElement", "secondValue"); + pojoEncoder.endEntity(); + pojoEncoder.literal("attribute", "value"); + pojoEncoder.endRecord(); + final StringListPojo stringListPojo = objectBuffer.pop(); + Assert.assertNotNull(stringListPojo); + Assert.assertEquals("value", stringListPojo.attribute); + final List strings = stringListPojo.stringList; + Assert.assertNotNull(strings); + Assert.assertEquals(2, strings.size()); + Assert.assertEquals("firstValue", strings.get(0)); + Assert.assertEquals("secondValue", strings.get(1)); + } + + public static class IntegerListPojo { + public List integerList; + } + + @Test + public void shouldEncodeEntityStreamToPojoWithIntegerList() { + final ObjectBuffer objectBuffer = new ObjectBuffer(); + final PojoEncoder pojoEncoder = new PojoEncoder( + IntegerListPojo.class); + pojoEncoder.setReceiver(objectBuffer); + pojoEncoder.startRecord("identifier"); + pojoEncoder.startEntity("integerList"); + pojoEncoder.literal("firstElement", "42"); + pojoEncoder.literal("firstElement", "23"); + pojoEncoder.endEntity(); + pojoEncoder.endRecord(); + final IntegerListPojo integerListPojo = objectBuffer.pop(); + Assert.assertNotNull(integerListPojo); + final List integers = integerListPojo.integerList; + Assert.assertNotNull(integers); + Assert.assertEquals(2, integers.size()); + Assert.assertEquals(42, integers.get(0).intValue()); + Assert.assertEquals(23, integers.get(1).intValue()); + } + + public static class SimplePojoListPojo { + private List simplePojoList; + + public void setSimplePojoList(final List simplePojoList) { + this.simplePojoList = simplePojoList; + } + } + + @Test + public void shouldEncodeEntityStreamToPojoWithSimplePojoList() { + final ObjectBuffer objectBuffer = new ObjectBuffer(); + final PojoEncoder pojoEncoder = new PojoEncoder( + SimplePojoListPojo.class); + pojoEncoder.setReceiver(objectBuffer); + pojoEncoder.startRecord("identifier"); + pojoEncoder.startEntity("simplePojoList"); + pojoEncoder.startEntity("simplePojo"); + pojoEncoder.literal("firstStringAttribute", "firstStringValue"); + pojoEncoder.literal("secondStringAttribute", "secondStringValue"); + pojoEncoder.endEntity(); + pojoEncoder.startEntity("simplePojo"); + pojoEncoder.literal("firstStringAttribute", "thirdValue"); + pojoEncoder.endEntity(); + pojoEncoder.endEntity(); + pojoEncoder.endRecord(); + final SimplePojoListPojo simplePojoListPojo = objectBuffer.pop(); + Assert.assertNotNull(simplePojoListPojo); + final List simplePojos = simplePojoListPojo.simplePojoList; + Assert.assertNotNull(simplePojos); + Assert.assertEquals(2, simplePojos.size()); + Assert.assertEquals("firstStringValue", + simplePojos.get(0).firstStringAttribute); + Assert.assertEquals("secondStringValue", simplePojos.get(0) + .getSecondStringAttribute()); + Assert.assertEquals("thirdValue", + simplePojos.get(1).firstStringAttribute); + } + +} From a5d502375d9825506f118d46c884f84fc4e10276 Mon Sep 17 00:00:00 2001 From: Thomas Seidel Date: Thu, 11 Dec 2014 18:31:13 +0100 Subject: [PATCH 3/3] Add sameEntity attribute to concat metamorph statement --- src/main/resources/schemata/metamorph.xsd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/schemata/metamorph.xsd b/src/main/resources/schemata/metamorph.xsd index bc9f8caa7..c6aab789f 100644 --- a/src/main/resources/schemata/metamorph.xsd +++ b/src/main/resources/schemata/metamorph.xsd @@ -329,6 +329,8 @@ +