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:
+ *
+ * Simple types like Strings or the primitive types produces a literal event
+ * with the member name as literal name and the member value as literal value
+ * Instances of {@link MetafactureSource} can insert user defined events
+ * into the stream by implementing the {@link MetafactureSource#sendToStream}
+ * method.
+ * Complex types like other pojos produces an entity with the member name as
+ * entity name and the decoded pojo members as the entity's elements
+ * Lists, Arrays and Sets produce events for each entry according to the
+ * rules above. Each of these events is named by the member name.
+ * Maps produces an entity with the member name as entity name. Each map
+ * entry produces a sub entity with the string representation of the entry key
+ * as entity name and the entry value decoded to the rules above.
+ *
+ *
+ *
+ *
+ * Here are some examples:
+ *
+ * {@code String str = "abc" -> literal("str", "abc")}
+ * {@code boolean bo = true -> literal("bo", "true")}
+ *
+ * {@code List li = ... ("a", "b") -> literal("li", "a"), literal("li", "b")}
+ *
+ * {@code String[] ar = ... ("a", "b") -> literal("ar", "a"), literal("ar", "b")}
+ *
+ *
+ * {@code Set se = ... ("a", "b") -> literal("se", "a"), literal("se", "b")}
+ *
+ *
+ * {@code Map ma = ... ("a" : "b", "c" : "d") -> startEntity("ma"), literal("a", "b"), literal("c", "d"), endEntity()}
+ *
+ *
+ *
+ *
+ *
+ * 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 @@
+