/*
* Copyright 2016 Christoph Böhme
*
* 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.io.StringReader;
import java.net.URL;
import org.culturegraph.mf.framework.StreamReceiver;
import org.xml.sax.InputSource;
/**
* Helper for including Metamorph scripts directly in Java code.
* <p>
* Typically, {@code InlineMoprh} is used as shown in the example to create a
* {@link Metamorph} instance based on an inline script:
* <pre>{@code
* Metamorph metamorph = InlineMorph.in(this)
* .with("<rules>")
* .with("<data source='test' />")
* .with("</rules>")
* .create();
* }</pre>
* <p>
* The morph script can either be complete xml document or a snippet containing
* only the inner part of the {@code <metamorph>} element.
* <p>
* The two different variants are distinguished by checking whether the script
* starts with “{@literal <?xml }” or
* “{@literal <metamorph }”. Leading whitespace is ignored. If it
* starts with an xml declaration, it is assumed that it is a
* complete xml document and the script is passed to {@link Metamorph} without
* modification. If it starts with a {@code <metamorph>} element, an xml
* declaration is added. If it starts with neither, the script is wrapped in a
* {@code <metamorph>} element and an xml declaration is added.
* <p>
* This class is primarily intended to simplify testing of Metamorph
* extensions.
*
* @author Christoph Böhme
*/
public final class InlineMorph {
private final StringBuilder scriptBuilder = new StringBuilder();
private String systemId;
private boolean needFinishMetamorphBoilerplate;
private InlineMorph() {
// Instance may only be created via #in(Object) or #in(Class)
}
/**
* Sets the object which contains the morph script.
* <p>
* This is an alternative to the {@link #in(Class)} method and has been
* defined for convenience.
*
* @param owner the object which contains the morph script
* @return an instance of {@code InlineMorph} for continuation
*/
public static InlineMorph in(Object owner) {
return in(owner.getClass());
}
/**
* Sets the class which contains the morph script. It will be used to create
* a system identifier for the morph script. The identifier is created by
* constructing a resource name from the class's package and converting it
* into a url. This allows inline scripts to refer to files that are placed
* in the same package as the class.
*
* @param owner the class which contains the morph script
* @return an instance of {@code InlineMorph} for continuation
*/
public static InlineMorph in(Class<?> owner) {
return new InlineMorph().setClassAsSystemId(owner);
}
private InlineMorph setClassAsSystemId(Class<?> owner) {
final URL baseUrl = owner.getResource("");
systemId = baseUrl.toExternalForm();
return this;
}
/**
* Adds a line to the morph script.
*
* @param line the next line of the morph script
* @return a reference to {@code this} for continuation
*/
public InlineMorph with(String line) {
if (scriptBuilder.length() == 0) {
appendBoilerplate(line);
}
scriptBuilder
.append(line)
.append("\n");
return this;
}
private void appendBoilerplate(String line) {
final String trimmedLine = line.trim();
if (!trimmedLine.startsWith("<?xml ")) {
appendXmlBoilerplate();
if (!trimmedLine.startsWith("<metamorph ")) {
appendMetamorphBoilerplate();
needFinishMetamorphBoilerplate = true;
}
}
}
private void appendXmlBoilerplate() {
scriptBuilder.append("<?xml version='1.1' encoding='UTF-8'?>\n");
}
private void appendMetamorphBoilerplate() {
scriptBuilder.append(
"<metamorph version='1'\n" +
" xmlns='http://www.culturegraph.org/metamorph'>");
}
/**
* Creates a {@link Metamorph} instance.
*
* @return a Metamorph object initialised with the inline script
*/
public Metamorph create() {
finishBoilerplate();
return new Metamorph(createScriptSource());
}
private void finishBoilerplate() {
if (needFinishMetamorphBoilerplate) {
scriptBuilder.append("</metamorph>\n");
needFinishMetamorphBoilerplate = false;
}
}
private InputSource createScriptSource() {
final InputSource scriptSource = new InputSource();
scriptSource.setSystemId(systemId);
scriptSource.setCharacterStream(new StringReader(scriptBuilder.toString()));
return scriptSource;
}
/**
* Creates a {@link Metamorph} instance. The {@code Metamorph} instance will
* be connected to the receiver passed as argument.
* <p>
* This is a convenience method. It is equivalent to calling {@link #create()}
* to create a {@link Metamorph} object and then call
* {@link Metamorph#setReceiver(StreamReceiver)} on the returned object.
*
* @param receiver downstream module to which the metamorph instance should
* send its output.
* @return a Metamorph object initialised with the inline script
*/
public Metamorph createConnectedTo(StreamReceiver receiver) {
final Metamorph metamorph = create();
metamorph.setReceiver(receiver);
return metamorph;
}
}