/* * Copyright (c) 2012 Fraunhofer IGD * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * Fraunhofer IGD */ package eu.esdihumboldt.hale.io.xslt.internal; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.Future; import javax.xml.XMLConstants; import javax.xml.namespace.NamespaceContext; import javax.xml.namespace.QName; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.exception.ParseErrorException; import org.apache.velocity.exception.ResourceNotFoundException; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Multimap; import com.google.common.io.ByteSink; import com.google.common.io.ByteSource; import com.google.common.io.ByteStreams; import com.google.common.io.Files; import eu.esdihumboldt.hale.common.align.model.Alignment; import eu.esdihumboldt.hale.common.align.model.Cell; import eu.esdihumboldt.hale.common.align.model.CellUtil; import eu.esdihumboldt.hale.common.align.model.Entity; import eu.esdihumboldt.hale.common.align.model.TransformationMode; import eu.esdihumboldt.hale.common.align.transformation.function.TransformationException; import eu.esdihumboldt.hale.common.core.io.ProgressIndicator; import eu.esdihumboldt.hale.common.core.io.project.ProjectInfo; import eu.esdihumboldt.hale.common.core.io.report.IOReport; import eu.esdihumboldt.hale.common.core.io.report.IOReporter; import eu.esdihumboldt.hale.common.core.io.report.impl.IOMessageImpl; import eu.esdihumboldt.hale.common.core.io.supplier.FileIOSupplier; import eu.esdihumboldt.hale.common.core.io.supplier.LocatableOutputSupplier; import eu.esdihumboldt.hale.common.schema.model.TypeDefinition; import eu.esdihumboldt.hale.io.gml.writer.internal.GmlWriterUtil; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.AbstractTypeMatcher; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.DefinitionPath; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.Descent; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.PathElement; import eu.esdihumboldt.hale.io.xsd.model.XmlElement; import eu.esdihumboldt.hale.io.xsd.model.XmlIndex; import eu.esdihumboldt.hale.io.xslt.SourceContextProvider; import eu.esdihumboldt.hale.io.xslt.XslPropertyTransformation; import eu.esdihumboldt.hale.io.xslt.XslTransformationUtil; import eu.esdihumboldt.hale.io.xslt.XslTypeTransformation; import eu.esdihumboldt.hale.io.xslt.XsltConstants; import eu.esdihumboldt.hale.io.xslt.XsltGenerationContext; import eu.esdihumboldt.hale.io.xslt.extension.XslPropertyTransformationExtension; import eu.esdihumboldt.hale.io.xslt.extension.XslTypeTransformationExtension; import eu.esdihumboldt.util.CustomIdentifiers; /** * Generate a XSLT transformation from an {@link Alignment}. Each generation * process has to use its own instance. * * @author Simon Templer */ @SuppressWarnings("restriction") public class XsltGenerator implements XsltConstants { private final XsltGenerationContext context = new XsltGenerationContext() { private int numIncludes = 0; private final Map<String, XslPropertyTransformation> cachedTransformations = new HashMap<String, XslPropertyTransformation>(); @Override public NamespaceContext getNamespaceContext() { return prefixes; } @Override public Alignment getAlignment() { return alignment; } @Override public XmlIndex getSourceSchema() { return sourceSchema; } @Override public XmlIndex getTargetSchema() { return targetSchema; } @Override public XslPropertyTransformation getPropertyTransformation(String functionId) { XslPropertyTransformation result = cachedTransformations.get(functionId); if (result == null) { try { result = XslPropertyTransformationExtension.getInstance() .getTransformation(functionId); } catch (Exception e) { return null; } cachedTransformations.put(functionId, result); } return result; } @Override public Template loadTemplate(Class<?> transformation, ByteSource resource, String id) throws ResourceNotFoundException, ParseErrorException, Exception { File templateFile = new File(workDir, "_" + transformation.getCanonicalName() + ((id == null) ? ("") : ("_" + id)) + ".xsl"); synchronized (ve) { if (!templateFile.exists()) { // copy template to template directory InputStream in = resource.openBufferedStream(); OutputStream out = new FileOutputStream(templateFile); try { ByteStreams.copy(in, out); } finally { out.close(); in.close(); } } } return ve.getTemplate(templateFile.getName(), "UTF-8"); } @Override public Template loadTemplate(final Class<?> transformation) throws Exception { return loadTemplate(transformation, new ByteSource() { @Override public InputStream openStream() throws IOException { return transformation .getResourceAsStream(transformation.getSimpleName() + ".xsl"); } }, null); } @Override public ByteSink addInclude() { String filename = "include_" + (++numIncludes) + ".xsl"; includes.add(filename); File file = new File(workDir, filename); return Files.asByteSink(file); } @Override public String reserveTemplateName(String desiredName) { synchronized (templateNames) { String id = desiredName; int num = 1; while (!isValidNewId(id)) { if (id.startsWith(cellIdentifiers.getPrefix())) { desiredName = '_' + desiredName; id = desiredName; } else { id = desiredName + '_' + num++; } } templateNames.add(id); return id; } } private boolean isValidNewId(String id) { if (id.startsWith(cellIdentifiers.getPrefix())) { // may not start with the cell identifiers prefix return false; } if (cellIdentifiers.getObject(id) != null) { // already used by a cell return false; } // may not be used if already reserved return !templateNames.contains(id); } @Override public String getInlineTemplateName(Cell typeCell) { return cellIdentifiers.getId(typeCell) + "_inline"; } @Override public String getSourceContext(TypeDefinition type) { String result = null; if (sourceContext != null) { result = sourceContext.getSourceContext(type, getNamespaceContext()); } if (result == null) { // default is anywhere in the document return "/"; } else { return result; } } }; /** * Fixed namespace prefixes. Prefixes mapped to namespaces. */ private static final Map<String, String> FIXED_PREFIXES = ImmutableMap.of( // NS_PREFIX_XSI, XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, // NS_PREFIX_XS, XMLConstants.W3C_XML_SCHEMA_NS_URI, // NS_PREFIX_XSL, NS_URI_XSL, // NS_PREFIX_CUSTOM_XSL, NS_CUSTOM_XSL, // NS_PREFIX_XSL_DEFINITIONS, NS_XSL_DEFINITIONS); /** * The template engine. */ private final VelocityEngine ve; /** * The reporter. */ private final IOReporter reporter; /** * The progress indicator. */ private final ProgressIndicator progress; /** * The alignment. */ private final Alignment alignment; /** * The target XML schema. */ private final XmlIndex targetSchema; /** * The source XML schema. */ private final XmlIndex sourceSchema; /** * The working directory where the templates reside. */ private final File workDir; /** * Collects XSL fragments to include in the main file */ private final Set<String> includes = new LinkedHashSet<String>(); /** * Namespace prefixes mapped to namespaces. */ private final NamespaceContextImpl prefixes; /** * The reserved template names. */ private final Set<String> templateNames = new HashSet<String>(); /** * The cell identifiers. */ private final CustomIdentifiers<Cell> cellIdentifiers = new CustomIdentifiers<Cell>(Cell.class, true) { @Override protected boolean isReserved(String id) { synchronized (templateNames) { // names in this set are reserved return templateNames.contains(id); } } }; /** * The name of the container in the target schema. */ private final XmlElement targetContainer; /** * An optional custom source context provider. */ private final SourceContextProvider sourceContext; private final ProjectInfo projectInfo; /** * Create a XSLT generator. * * @param workDir the working directory where the generator may store * temporary files, the caller is responsible for cleaning this * up, e.g. after {@link #write(LocatableOutputSupplier)} was * called * @param alignment the alignment * @param sourceSchema the source schema * @param targetSchema the target schema * @param reporter the reporter for documenting errors * @param progress the progress indicator for indicating the generation * progress * @param containerElement the name of the element to serve as document root * in the target XML file * @param sourceContext an optional custom source context provider * @param projectInfo the project info, if available, may be * <code>null</code> * @throws Exception if an error occurs initializing the generator */ public XsltGenerator(File workDir, Alignment alignment, XmlIndex sourceSchema, XmlIndex targetSchema, IOReporter reporter, ProgressIndicator progress, XmlElement containerElement, SourceContextProvider sourceContext, ProjectInfo projectInfo) throws Exception { this.reporter = reporter; this.progress = progress; this.alignment = alignment; this.workDir = workDir; this.targetContainer = containerElement; this.targetSchema = targetSchema; this.sourceSchema = sourceSchema; this.sourceContext = sourceContext; this.projectInfo = projectInfo; // initialize the velocity template engine Templates.copyTemplates(workDir); ve = new VelocityEngine(); // ve.setProperty("resource.loader", "main, file"); // ve.setProperty("main.resource.loader.class", // eu.esdihumboldt.hale.io.xslt.internal.Templates.class); // custom resource loader does not work in OSGi context, so copy // templates to template folder ve.setProperty(VelocityEngine.FILE_RESOURCE_LOADER_PATH, workDir.getAbsolutePath()); // custom logger ve.setProperty(VelocityEngine.RUNTIME_LOG_LOGSYSTEM, new AVelocityLogger()); ve.init(); // initialize the prefix map NamespaceContextImpl prefixes = new NamespaceContextImpl(); // fixed prefixes for (Entry<String, String> entry : FIXED_PREFIXES.entrySet()) { prefixes.add(entry.getKey(), entry.getValue()); } // target schema prefixes for (Entry<String, String> pair : this.targetSchema.getPrefixes().entrySet()) { prefixes.add(pair.getValue(), pair.getKey()); } // source schema prefixes for (Entry<String, String> pair : this.sourceSchema.getPrefixes().entrySet()) { prefixes.add(pair.getValue(), pair.getKey()); } this.prefixes = prefixes; } /** * Generate the XSLT transformation and write it to the given target. * * @param target the target output supplier * @return the report * @throws Exception if a unrecoverable error occurs during the process */ public IOReport write(LocatableOutputSupplier<? extends OutputStream> target) throws Exception { Template root = ve.getTemplate(Templates.ROOT, "UTF-8"); VelocityContext context = XslTransformationUtil.createStrictVelocityContext(); // project info context.put("info", ProjectXslInfo.getInfo(projectInfo)); // collects IDs of type cells Set<String> typeIds = new HashSet<String>(); // type cells for (Cell typeCell : alignment.getTypeCells()) { if (typeCell.getTransformationMode() != TransformationMode.disabled) { // ignore disabled cells Entity targetEntity = CellUtil.getFirstEntity(typeCell.getTarget()); if (targetEntity != null) { // assign identifiers for type transformations String targetName = targetEntity.getDefinition().getDefinition().getName() .getLocalPart(); String id = cellIdentifiers.getId(typeCell, targetName); typeIds.add(id); } else { reporter.warn( new IOMessageImpl("Ignoring type relation without target type", null)); } } } // collects IDs of type cells mapped to target element names Map<String, QName> targetElements = new HashMap<String, QName>(); // container File container = new File(workDir, "container.xsl"); progress.setCurrentTask("Generating container"); generateContainer(typeIds, container, targetElements); Set<String> passiveCellIds = new HashSet<String>(typeIds); progress.setCurrentTask("Generate type transformations"); // all active cells templates for (Entry<String, QName> entry : targetElements.entrySet()) { // generate XSL fragments for type transformations String id = entry.getKey(); QName elementName = entry.getValue(); Cell typeCell = cellIdentifiers.getObject(id); // this is not a passive cell passiveCellIds.remove(id); XmlElement targetElement = targetSchema.getElements().get(elementName); String filename = "_" + id + ".xsl"; File file = new File(workDir, filename); includes.add(filename); generateTypeTransformation(id, targetElement, typeCell, file); } // all passive cell templates for (String passiveId : passiveCellIds) { Cell typeCell = cellIdentifiers.getObject(passiveId); String filename = "_" + passiveId + ".xsl"; File file = new File(workDir, filename); includes.add(filename); // XXX dummy target element XmlElement targetElement = new XmlElement(new QName(NS_XSL_DEFINITIONS, "dummy"), null, null); generateTypeTransformation(passiveId, targetElement, typeCell, file); // for passive cells no variables should be created typeIds.remove(passiveId); } // namespaces that occur additionally to the fixed namespaces Map<String, String> additionalNamespaces = new HashMap<String, String>(prefixes.asMap()); for (String fixedPrefix : FIXED_PREFIXES.keySet()) { additionalNamespaces.remove(fixedPrefix); } context.put("additionalNamespaces", additionalNamespaces); // types cells /* * The type identifiers are used as variable name to store the result of * the equally named template. */ context.put("targets", typeIds); // includes // TODO check if files to include are actually there? context.put("includes", includes); OutputStream out = target.getOutput(); XMLPrettyPrinter printer = new XMLPrettyPrinter(out); Future<?> ready = printer.start(); Writer writer = new OutputStreamWriter(printer, "UTF-8"); try { root.merge(context, writer); writer.flush(); } finally { writer.close(); ready.get(); out.close(); } reporter.setSuccess(reporter.getErrors().isEmpty()); return reporter; } /** * Generate a XSL fragment that is the root of transformed target files and * incorporates the results of type transformation that are store as * temporary documents in XSL variables. * * @param typeIds the identifiers of the type transformations, they are also * the names of the variables holding the temporary documents * @param templateFile the file to write the fragment to * @param targetElements an empty map that is populated with variable names * mapped to target element names * @throws IOException if an error occurs writing the template * @throws XMLStreamException if an error occurs writing XML content to the * template */ protected void generateContainer(Set<String> typeIds, File templateFile, Map<String, QName> targetElements) throws XMLStreamException, IOException { // group typeIds by target type Multimap<TypeDefinition, String> groupedResults = HashMultimap.create(); for (String typeId : typeIds) { Cell cell = cellIdentifiers.getObject(typeId); if (cell.getTransformationMode() == TransformationMode.active) { // only active cells get placed in the container Collection<? extends Entity> targetEntities = cell.getTarget().values(); if (targetEntities.size() == 1) { TypeDefinition type = targetEntities.iterator().next().getDefinition() .getType(); groupedResults.put(type, typeId); } else { throw new IllegalStateException( "Type cell may only have exactly one target type"); } } } // generate container and integration of temporary documents writeContainerFragment(templateFile, groupedResults, targetElements); } /** * Write the container fragment. * * @param templateFile the file to write to * @param groupedResults the result variable names grouped by associated * target type * @param targetElements an empty map that is populated with variable names * mapped to target element names * @throws IOException if an error occurs writing the template * @throws XMLStreamException if an error occurs writing XML content to the * template */ private void writeContainerFragment(File templateFile, Multimap<TypeDefinition, String> groupedResults, Map<String, QName> targetElements) throws XMLStreamException, IOException { XMLStreamWriter writer = XslTransformationUtil.setupXMLWriter( new BufferedOutputStream(new FileOutputStream(templateFile)), prefixes); try { // write container GmlWriterUtil.writeStartElement(writer, targetContainer.getName()); // generate an eventual required identifier on the container GmlWriterUtil.writeRequiredID(writer, targetContainer.getType(), null, false); writeContainerIntro(writer, context); // cache definition paths Map<TypeDefinition, DefinitionPath> paths = new HashMap<TypeDefinition, DefinitionPath>(); Descent lastDescent = null; for (Entry<TypeDefinition, String> entry : groupedResults.entries()) { TypeDefinition type = entry.getKey(); // get stored definition path for the type DefinitionPath defPath; if (paths.containsKey(type)) { // get the stored path, may be null defPath = paths.get(type); } else { // determine a valid definition path in the container defPath = findMemberAttribute(targetContainer, type); // store path (may be null) paths.put(type, defPath); } if (defPath != null) { // insert xsl:for-each at the appropriate position in // the path defPath = pathInsertForEach(defPath, entry.getValue(), targetElements); lastDescent = Descent.descend(writer, defPath, lastDescent, false, true); // write single target instance from variable GmlWriterUtil.writeEmptyElement(writer, new QName(NS_URI_XSL, "copy-of")); writer.writeAttribute("select", "."); } else { reporter.warn(new IOMessageImpl(MessageFormat.format( "No compatible member attribute for type {0} found in root element {1}, one instance was skipped", type.getDisplayName(), targetContainer.getName().getLocalPart()), null)); } } if (lastDescent != null) { lastDescent.close(); } // end container writer.writeEndElement(); } finally { writer.close(); } } /** * Write additional content into the container before it is populated by the * type relations. * * @param writer the XML stream writer * @param context the XSLT generation context * @throws XMLStreamException if an error occurs while writing to the * container * @throws IOException if an error occurs writing to the file */ @SuppressWarnings("unused") protected void writeContainerIntro(XMLStreamWriter writer, XsltGenerationContext context) throws XMLStreamException, IOException { // override me } /** * Inserts a <code>xsl:for-each</code> element in the path before the * element that may be repeated. Also removes the last path element. * * @param path the path where target instances should be written to * @param variable the variable name of the xsl:variable holding the * instances * @param targetElements an empty map that is populated with variable names * mapped to target element names * @return the adapted path including the for-each instruction and w/o the * last path element */ private DefinitionPath pathInsertForEach(DefinitionPath path, String variable, Map<String, QName> targetElements) { List<PathElement> elements = new ArrayList<PathElement>(path.getSteps()); int index = elements.size() - 1; PathElement lastNonUniqueElement = null; while (lastNonUniqueElement == null && index >= 0) { PathElement element = elements.get(index); if (!element.isUnique()) { lastNonUniqueElement = element; } else { index--; } } if (lastNonUniqueElement == null) { // TODO instead some fall-back? throw new IllegalStateException("No element in member path repeatable"); } /* * Store last element name for variable, this information is needed for * the type transformation. */ targetElements.put(variable, elements.get(elements.size() - 1).getName()); // remove last element elements.remove(elements.size() - 1); // insert for-each element before last non-unique element if (index == elements.size()) { /* * There seems to be a problem with compiling the XSLT if the * for-each is the last element in the path. Then insert the whole * variable in once piece. */ elements.add(index, new XslForEach("$" + variable)); } else { // loop for each element in the variable elements.add(index, new XslForEach("$" + variable + "/*")); } return new DefinitionPath(elements); } /** * Find a matching attribute for the given member type in the given * container type * * @param container the container element * @param memberType the member type * * @return the attribute definition or <code>null</code> */ protected DefinitionPath findMemberAttribute(XmlElement container, final TypeDefinition memberType) { AbstractTypeMatcher<TypeDefinition> matcher = new AbstractTypeMatcher<TypeDefinition>() { @Override protected DefinitionPath matchPath(TypeDefinition type, TypeDefinition matchParam, DefinitionPath path) { if (type.equals(memberType)) { return path; } return null; } }; // candidate match List<DefinitionPath> candidates = matcher.findCandidates(container.getType(), container.getName(), true, memberType); if (candidates != null && !candidates.isEmpty()) { return candidates.get(0); // TODO notification? FIXME will this // work? possible problem: attribute is // selected even though better candidate // is in other attribute } return null; } /** * Generate a XSL fragment for transformation based on the given type * relation. * * @param templateName name of the XSL template * @param typeCell the type relation * @param targetfile the target file to write the fragment to * @param targetElement the target element to use to hold a transformed * instance * @throws TransformationException if an unrecoverable error occurs during * the XSLT transformation generation */ protected void generateTypeTransformation(String templateName, XmlElement targetElement, Cell typeCell, File targetfile) throws TransformationException { XslTypeTransformation xslt; try { xslt = XslTypeTransformationExtension.getInstance() .getTransformation(typeCell.getTransformationIdentifier()); } catch (Exception e) { throw new TransformationException( "Could not retrieve XSLT transformation generator for cell function", e); } xslt.setContext(context); xslt.generateTemplate(templateName, targetElement, typeCell, new FileIOSupplier(targetfile)); } }