/* * 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.metamorph; import java.util.Deque; import java.util.LinkedList; import java.util.Map; import java.util.regex.Pattern; import org.culturegraph.mf.commons.reflection.ConfigurableClass; import org.culturegraph.mf.commons.reflection.ReflectionUtil; import org.culturegraph.mf.metamorph.api.Collect; import org.culturegraph.mf.metamorph.api.ConditionAware; import org.culturegraph.mf.metamorph.api.FlushListener; import org.culturegraph.mf.metamorph.api.Function; import org.culturegraph.mf.metamorph.api.InterceptorFactory; import org.culturegraph.mf.metamorph.api.Maps; import org.culturegraph.mf.metamorph.api.MorphBuildException; import org.culturegraph.mf.metamorph.api.NamedValuePipe; import org.culturegraph.mf.metamorph.xml.Location; import org.w3c.dom.Node; /** * Builds a {@link Metamorph} from an xml description * * @author Markus Michael Geipel * @author Christoph Böhme * */ public final class MorphBuilder extends AbstractMetamorphDomWalker { private static final String NOT_FOUND = " not found."; private static final String JAVA = "java"; private static final String JAVAMAP = "javamap"; private static final String RECORD = "record"; private static final String OR_STRING = "|"; private static final Pattern OR_PATTERN = Pattern.compile(OR_STRING, Pattern.LITERAL); private final Metamorph metamorph; private final InterceptorFactory interceptorFactory; private final Deque<StackFrame> stack = new LinkedList<StackFrame>(); private static final class StackFrame { private final NamedValuePipe headPipe; private NamedValuePipe pipe; private boolean inEntityName; private boolean inCondition; public StackFrame(final NamedValuePipe headPipe) { this.headPipe = headPipe; this.pipe = headPipe; } public NamedValuePipe getHeadPipe() { return headPipe; } public void setPipe(final NamedValuePipe pipe) { this.pipe = pipe; } public NamedValuePipe getPipe() { return pipe; } public void setInEntityName(final boolean inEntityName) { this.inEntityName = inEntityName; } public boolean isInEntityName() { return inEntityName; } public void setInCondition(final boolean inCondition) { this.inCondition = inCondition; } public boolean isInCondition() { return inCondition; } } protected MorphBuilder(final Metamorph metamorph, final InterceptorFactory interceptorFactory) { super(); this.metamorph = metamorph; this.interceptorFactory = interceptorFactory; stack.push(new StackFrame(metamorph)); } @Override protected void setEntityMarker(final String entityMarker) { if (null != entityMarker) { metamorph.setEntityMarker(entityMarker); } } @Override protected void handleInternalMap(final Node mapNode) { final String mapName = resolvedAttribute(mapNode, AttributeName.NAME); final String mapDefault = resolvedAttribute(mapNode, AttributeName.DEFAULT); for (Node entryNode = mapNode.getFirstChild(); entryNode != null; entryNode = entryNode.getNextSibling()) { final String entryName = resolvedAttribute(entryNode, AttributeName.NAME); final String entryValue = resolvedAttribute(entryNode, AttributeName.VALUE); metamorph.putValue(mapName, entryName, entryValue); } if (mapDefault != null) { metamorph.putValue(mapName, Maps.DEFAULT_MAP_KEY, mapDefault); } } @SuppressWarnings("unchecked") @Override protected void handleMapClass(final Node mapNode) { final Map<String, String> attributes = resolvedAttributeMap(mapNode); final String mapName = resolveVars(attributes.remove(AttributeName.NAME.getString())); final Map<String, String> map; if (mapNode.getLocalName().equals(JAVAMAP)) { final String className = resolvedAttribute(mapNode, AttributeName.CLASS); attributes.remove(AttributeName.CLASS.getString()); final ConfigurableClass<? extends Map> mapClass = ReflectionUtil.loadClass(className, Map.class); map = mapClass.newInstance(attributes); } else if (getMapFactory().containsKey(mapNode.getLocalName())) { map = getMapFactory().newInstance(mapNode.getLocalName(), attributes); } else { throw new MorphBuildException("Map " + mapNode.getLocalName() + NOT_FOUND); } metamorph.putMap(mapName, map); } @Override @SuppressWarnings("unchecked") // protected by 'if (Function.class.isAssignableFrom(clazz))' protected void handleFunctionDefinition(final Node functionDefNode) { final Class<?> clazz; final String className = resolvedAttribute(functionDefNode, AttributeName.CLASS); try { clazz = Thread.currentThread().getContextClassLoader().loadClass(className); } catch (final ClassNotFoundException e) { throw new MorphBuildException("Function " + className + NOT_FOUND, e); } if (Function.class.isAssignableFrom(clazz)) { getFunctionFactory().registerClass(resolvedAttribute(functionDefNode, AttributeName.NAME), (Class<Function>) clazz); } else { throw new MorphBuildException(className + " does not implement interface 'Function'"); } } @Override protected void handleMetaEntry(final String name, final String value) { metamorph.putValue(Metamorph.METADATA, name, value); } @Override protected void init() { // nothing to do } @Override protected void finish() { // nothing to do } @Override protected void enterData(final Node dataNode) { final Data data = new Data(); data.setName(resolvedAttribute(dataNode, AttributeName.NAME)); data.setSourceLocation(getSourceLocation(dataNode)); final NamedValuePipe interceptor = interceptorFactory.createNamedValueInterceptor(); final NamedValuePipe delegate; if (interceptor == null) { delegate = data; } else { delegate = interceptor; data.addNamedValueSource(delegate); } final String source = resolvedAttribute(dataNode, AttributeName.SOURCE); metamorph.registerNamedValueReceiver(source, delegate); stack.push(new StackFrame(data)); } @Override protected void exitData(final Node node) { final NamedValuePipe dataPipe = stack.pop().getPipe(); final NamedValuePipe interceptor = interceptorFactory.createNamedValueInterceptor(); final NamedValuePipe delegate; if (interceptor == null) { delegate = dataPipe; } else { delegate = interceptor; delegate.addNamedValueSource(dataPipe); } final StackFrame parent = stack.peek(); if (parent.isInEntityName()) { // Protected xsd schema and by assertion in enterName: ((Entity) parent.getPipe()).setNameSource(delegate); } else if (parent.isInCondition()) { // Protected xsd schema and by assertion in enterIf: ((ConditionAware) parent.getPipe()).setConditionSource(delegate); } else { parent.getPipe().addNamedValueSource(delegate); } } @Override protected void enterName(final Node nameNode) { assert stack.peek().getPipe() instanceof Entity : "statement `name` is only allowed in `entity` statements"; stack.peek().setInEntityName(true); } @Override protected void exitName(final Node nameNode) { stack.peek().setInEntityName(false); } @Override protected void enterIf(final Node nameNode) { assert stack.peek().getPipe() instanceof ConditionAware : "statement `if` is not allowed in the current element"; stack.peek().setInCondition(true); } @Override protected void exitIf(final Node nameNode) { stack.peek().setInCondition(false); } @Override protected void enterCollect(final Node node) { final Map<String, String> attributes = resolvedAttributeMap(node); // flushWith should not be passed to the headPipe object via a // setter (see newInstance): attributes.remove(AttributeName.FLUSH_WITH.getString()); if (!getCollectFactory().containsKey(node.getLocalName())) { throw new MorphBuildException("Collector " + node.getLocalName() + NOT_FOUND); } final Collect collect; if (ENTITY.equals(node.getLocalName())) { collect = getCollectFactory().newInstance(node.getLocalName(), attributes, metamorph); } else { collect = getCollectFactory().newInstance(node.getLocalName(), attributes); } collect.setSourceLocation(getSourceLocation(node)); stack.push(new StackFrame(collect)); } @Override protected void exitCollect(final Node node) { final StackFrame currentCollect = stack.pop(); final Collect collector = (Collect) currentCollect.getHeadPipe(); final NamedValuePipe tailPipe = currentCollect.getPipe(); final NamedValuePipe interceptor = interceptorFactory.createNamedValueInterceptor(); final NamedValuePipe delegate; if (interceptor == null || tailPipe instanceof Entity) { // The result of entity collectors cannot be intercepted // because they only use the receive/emit interface for // signalling while the actual data is transferred using // a custom mechanism. In order for this to work the Entity // class checks whether source and receiver are an // instances of Entity. If an interceptor is inserted between // entity elements this mechanism will break. delegate = tailPipe; } else { delegate = interceptor; delegate.addNamedValueSource(tailPipe); } final StackFrame parent = stack.peek(); if (parent.isInEntityName()) { // Protected xsd schema and by assertion in enterName: ((Entity) parent.getPipe()).setNameSource(delegate); } else if (parent.isInCondition()) { // Protected xsd schema and by assertion in enterIf: ((ConditionAware) parent.getPipe()).setConditionSource(delegate); } else { parent.getPipe().addNamedValueSource(delegate); } // must be set after recursive calls to flush descendants before parent final String flushWith = resolvedAttribute(node, AttributeName.FLUSH_WITH); if (null != flushWith) { collector.setWaitForFlush(true); registerFlush(flushWith, collector); } } private void registerFlush(final String flushWith, final FlushListener flushListener) { final String[] keysSplit = OR_PATTERN.split(flushWith); for (final String key : keysSplit) { final FlushListener interceptor = interceptorFactory.createFlushInterceptor(flushListener); final FlushListener delegate; if (interceptor == null) { delegate = flushListener; } else { delegate = interceptor; } if (key.equals(RECORD)) { metamorph.registerRecordEndFlush(delegate); } else { metamorph.registerNamedValueReceiver(key, new Flush(delegate)); } } } @Override protected void handleFunction(final Node functionNode) { final Function function; final Map<String, String> attributes = resolvedAttributeMap(functionNode); if (functionNode.getLocalName().equals(JAVA)) { final String className = resolvedAttribute(functionNode, AttributeName.CLASS); attributes.remove(AttributeName.CLASS.getString()); final ConfigurableClass<? extends Function> functionClass = ReflectionUtil.loadClass(className, Function.class); function = functionClass.newInstance(attributes); } else if (getFunctionFactory().containsKey(functionNode.getLocalName())) { final String flushWith = attributes.remove(AttributeName.FLUSH_WITH.getString()); function = getFunctionFactory().newInstance(functionNode.getLocalName(), attributes); if (null != flushWith) { registerFlush(flushWith, function); } } else { throw new MorphBuildException(functionNode.getLocalName() + NOT_FOUND); } function.setSourceLocation(getSourceLocation(functionNode)); function.setMaps(metamorph); // add key value entries... for (Node mapEntryNode = functionNode.getFirstChild(); mapEntryNode != null; mapEntryNode = mapEntryNode .getNextSibling()) { final String entryName = resolvedAttribute(mapEntryNode, AttributeName.NAME); final String entryValue = resolvedAttribute(mapEntryNode, AttributeName.VALUE); function.putValue(entryName, entryValue); } final StackFrame head = stack.peek(); final NamedValuePipe interceptor = interceptorFactory.createNamedValueInterceptor(); final NamedValuePipe delegate; if (interceptor == null) { delegate = function; } else { delegate = interceptor; function.addNamedValueSource(delegate); } delegate.addNamedValueSource(head.getPipe()); head.setPipe(function); } private XmlSourceLocation getSourceLocation(final Node node) { return new XmlSourceLocation((Location) node.getUserData( Location.USER_DATA_ID)); } }