You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2021/11/10 09:19:09 UTC

[isis] branch master updated: ISIS-2890: split DocxService into interface and implementation

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/master by this push:
     new 71ed932  ISIS-2890: split DocxService into interface and implementation
71ed932 is described below

commit 71ed932a0a0b02e8dc030844722f1ce7ebc19d11
Author: Andi Huber <ah...@apache.org>
AuthorDate: Wed Nov 10 10:19:01 2021 +0100

    ISIS-2890: split DocxService into interface and implementation
    
    - then properly register with Spring
---
 .../isis/subdomains/docx/applib/DocxService.java   | 396 ++-------------------
 .../applib/IsisModuleSubdomainsDocxApplib.java     |   6 +
 .../DocxServiceDefault.java}                       | 162 +--------
 .../isis/subdomains/docx/applib/util/Docx.java     |  17 +-
 .../isis/subdomains/docx/applib/util/Dump.java     |  28 +-
 .../isis/subdomains/docx/applib/util/Jdom2.java    |  20 +-
 .../isis/subdomains/docx/applib/util/Types.java    |   2 +-
 .../docx/applib/DocxService_merge_Test.java        |  13 +-
 8 files changed, 87 insertions(+), 557 deletions(-)

diff --git a/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/DocxService.java b/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/DocxService.java
index 3d1a25a..c747328 100644
--- a/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/DocxService.java
+++ b/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/DocxService.java
@@ -1,83 +1,47 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you 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.apache.isis.subdomains.docx.applib;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.ArrayList;
 import java.util.List;
-import java.util.stream.Collectors;
 
-import org.apache.commons.compress.utils.Lists;
-import org.apache.commons.io.IOUtils;
-import org.docx4j.Docx4J;
-import org.docx4j.XmlUtils;
-import org.docx4j.com.google.common.base.Objects;
-import org.docx4j.convert.out.FOSettings;
-import org.docx4j.fonts.IdentityPlusMapper;
-import org.docx4j.fonts.Mapper;
-import org.docx4j.openpackaging.exceptions.Docx4JException;
 import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
-import org.docx4j.wml.Body;
-import org.docx4j.wml.P;
-import org.docx4j.wml.R;
-import org.docx4j.wml.SdtElement;
-import org.docx4j.wml.Tbl;
-import org.docx4j.wml.Tc;
-import org.docx4j.wml.Tr;
-import org.jdom2.Content;
-import org.jdom2.Element;
-import org.jdom2.input.DOMBuilder;
-import org.springframework.stereotype.Service;
-import org.w3c.dom.Document;
 
 import org.apache.isis.subdomains.docx.applib.exceptions.LoadInputException;
 import org.apache.isis.subdomains.docx.applib.exceptions.LoadTemplateException;
 import org.apache.isis.subdomains.docx.applib.exceptions.MergeException;
-import org.apache.isis.subdomains.docx.applib.traverse.AllMatches;
-import org.apache.isis.subdomains.docx.applib.traverse.FirstMatch;
-import org.apache.isis.subdomains.docx.applib.util.Docx;
-import org.apache.isis.subdomains.docx.applib.util.Jdom2;
-import org.apache.isis.subdomains.docx.applib.util.Types;
 
 import lombok.Builder;
 import lombok.Getter;
-import lombok.val;
 
 /**
- * Provides a mail-merge capability.
+ * Merges input HTML against a provided <i>docx</i> template, generating a <i>Word docx</i>.
  *
  * @since 2.x {@index}
  */
-@Service
-public class DocxService {
+public interface DocxService {
+
+    /**
+     * Load and return an in-memory representation of a docx.
+     *
+     * <p>
+     * This is public API because building the in-memory structure can be
+     * quite slow.  Thus, clients can use this method to cache the in-memory
+     * structure, and pass it in the {@link MergeParams} (through the
+     * {@link MergeParams.Builder#docxTemplateAsWpMlPackage(WordprocessingMLPackage) builder method})
+     */
+    WordprocessingMLPackage loadPackage(InputStream docxTemplate) throws LoadTemplateException;
+
+    /**
+     * Merge the input arguments (as HTML) against the Docx template, writing out as a Word docx..
+     */
+    void merge(MergeParams mergeDefn) throws LoadInputException, LoadTemplateException, MergeException;
 
     /**
      * @since 2.x {@index}
      */
     @Getter
     @Builder(builderClassName = "Builder")
-    public static class MergeParams {
+    static class MergeParams {
 
         /**
          * Defines the policy for matching input to placeholders.
@@ -153,127 +117,10 @@ public class DocxService {
     }
 
     /**
-     * Load and return an in-memory representation of a docx.
-     *
-     * <p>
-     * This is public API because building the in-memory structure can be
-     * quite slow.  Thus, clients can use this method to cache the in-memory
-     * structure, and pass it in the {@link MergeParams} (through the
-     * {@link MergeParams.Builder#docxTemplateAsWpMlPackage(WordprocessingMLPackage) builder method})
-     */
-    public WordprocessingMLPackage loadPackage(final InputStream docxTemplate) throws LoadTemplateException {
-        final WordprocessingMLPackage docxPkg;
-        try {
-            docxPkg = WordprocessingMLPackage.load(docxTemplate);
-        } catch (final Docx4JException ex) {
-            throw new LoadTemplateException("Unable to load docx template from input stream", ex);
-        }
-        return docxPkg;
-    }
-
-    /**
-     * Merge the input arguments (as HTML) against the Docx template, writing out as a Word docx..
-     */
-    public void merge(final MergeParams mergeDefn) throws LoadInputException, LoadTemplateException, MergeException {
-
-        final org.jdom2.Document htmlJdomDoc;
-        final Document inputAsHtmlDoc = mergeDefn.getInputAsHtmlDoc();
-        final String inputAsHtml = mergeDefn.getInputAsHtml();
-        if(inputAsHtmlDoc != null) {
-            htmlJdomDoc = new DOMBuilder().build(inputAsHtmlDoc);
-        } else if (inputAsHtml != null) {
-            htmlJdomDoc = Jdom2.loadInput(inputAsHtml);
-        } else {
-            throw new IllegalArgumentException("Input HTML must be provided");
-        }
-
-        final DefensiveCopy defensiveCopy;
-        final WordprocessingMLPackage docxPkg;
-        final WordprocessingMLPackage docxTemplateAsWpMlPackage = mergeDefn.getDocxTemplateAsWpMlPackage();
-        final InputStream docxTemplate = mergeDefn.getDocxTemplate();
-        if(docxTemplateAsWpMlPackage != null) {
-            docxPkg = docxTemplateAsWpMlPackage;
-            defensiveCopy = DefensiveCopy.REQUIRED;
-        } else if (docxTemplate != null) {
-            docxPkg = loadPackage(docxTemplate);
-            defensiveCopy = DefensiveCopy.NOT_REQUIRED;
-        } else {
-            throw new IllegalArgumentException("Docx template HTML must be provided");
-        }
-
-        val output = mergeDefn.getOutput();
-        if(output == null) {
-            throw new IllegalArgumentException("Output stream must be provided");
-        }
-        merge(htmlJdomDoc, docxPkg, output, mergeDefn.getMatchingPolicy(), defensiveCopy, mergeDefn.getOutputType());
-    }
-
-    private void merge(
-            final org.jdom2.Document htmlDoc,
-            final WordprocessingMLPackage docxTemplateInput,
-            final OutputStream docxTarget,
-            final MatchingPolicy matchingPolicy,
-            final DefensiveCopy defensiveCopy,
-            final OutputType outputType)
-            throws MergeException {
-
-        final WordprocessingMLPackage docxTemplate =
-                defensiveCopy == DefensiveCopy.REQUIRED
-                        ? Docx.clone(docxTemplateInput)
-                        : docxTemplateInput;
-
-        try {
-            final Element bodyEl = Jdom2.htmlBodyFor(htmlDoc);
-            final Body docXBody = Docx.docxBodyFor(docxTemplate);
-
-            merge(bodyEl, docXBody, matchingPolicy);
-
-            if (outputType == OutputType.PDF) {
-
-
-                final FOSettings foSettings = Docx4J.createFOSettings();
-                foSettings.setWmlPackage(docxTemplate);
-
-                try {
-                    final Mapper fontMapper = new IdentityPlusMapper();
-                    docxTemplate.setFontMapper(fontMapper, true);
-                } catch (final Exception e) {
-                    throw new MergeException("unable to set font mapper for PDF generation", e);
-                }
-
-                // according to the documentation/examples the XSL transformation
-                // is slower but more feature complete than Docx4J.FLAG_EXPORT_PREFER_NONXSL
-
-                final int flags = Docx4J.FLAG_EXPORT_PREFER_XSL;
-
-                Docx4J.toFO(foSettings, docxTarget, flags);
-
-            } else {
-                final File tempTargetFile = createTempFile();
-                FileInputStream tempTargetFis = null;
-                try {
-                    docxTemplate.save(tempTargetFile);
-                    tempTargetFis = new FileInputStream(tempTargetFile);
-                    IOUtils.copy(tempTargetFis, docxTarget);
-                } finally {
-                    IOUtils.closeQuietly(tempTargetFis);
-                    tempTargetFile.delete();
-                }
-            }
-        } catch (final Docx4JException e) {
-            throw new MergeException("unable to write to target file", e);
-        } catch (final FileNotFoundException e) {
-            throw new MergeException("unable to read back from target file", e);
-        } catch (final IOException e) {
-            throw new MergeException("unable to generate output stream from temporary file", e);
-        }
-    }
-
-    /**
      * Defines the strategy as to whether placeholders must exactly input data
      * (or whether there can be unmatched placeholders, or conversely unused input data).
      */
-    public enum MatchingPolicy {
+    enum MatchingPolicy {
         STRICT(false, false),
         ALLOW_UNMATCHED_INPUT(true, false),
         ALLOW_UNMATCHED_PLACEHOLDERS(false, true),
@@ -302,15 +149,10 @@ public class DocxService {
         }
     }
 
-    private enum DefensiveCopy {
-        REQUIRED,
-        NOT_REQUIRED
-    }
-
     /**
      * The type of the file to generate
      */
-    public enum OutputType {
+    enum OutputType {
         DOCX,
         /**
          * Support for PDF should be considered experimental.
@@ -318,198 +160,4 @@ public class DocxService {
         PDF
     }
 
-    private enum MergeType {
-        PLAIN("p.plain"),
-        RICH("p.rich"),
-        DATE("p.date"),
-        UL("ul") {
-            @Override
-            boolean merge(final Element htmlUl, final SdtElement sdtElement) {
-                final List<Element> htmlLiList = htmlUl.getChildren("li"); // can be empty
-
-                final List<P> docxPOrigList = AllMatches.<P>matching(sdtElement, Types.withType(P.class));
-                if (docxPOrigList.isEmpty()) {
-                    return false;
-                }
-
-                final List<P> docxPNewList = new ArrayList<P>();
-                for (final Element htmlLi : htmlLiList) {
-                    final List<Element> htmlPList = htmlLi.getChildren("p");
-
-                    for (int htmlPNum = 0; htmlPNum < htmlPList.size(); htmlPNum++) {
-                        final int numDocxPNum = docxPOrigList.size();
-                        final int docxPNum = numDocxPNum == 1 || htmlPNum == 0 ? 0 : 1;
-                        final P docxP = XmlUtils.deepCopy(docxPOrigList.get(docxPNum));
-                        docxPNewList.add(docxP);
-                        final R docxR = FirstMatch.<R>matching(docxP, Types.withType(R.class));
-                        final Element htmlP = htmlPList.get(htmlPNum);
-                        Docx.setText(docxR, Jdom2.textValueOf(htmlP));
-                    }
-                }
-
-                // remove original and replace with new
-                final List<Object> content = sdtElement.getSdtContent().getContent();
-                for (final P docxP : docxPOrigList) {
-                    content.remove(docxP);
-                }
-                for (final P docxP : docxPNewList) {
-                    content.add(docxP);
-                }
-                return true;
-            }
-        },
-        TABLE("table") {
-            @Override
-            boolean merge(final Element htmlTable, final SdtElement sdtElement) {
-
-                final List<Element> htmlTrOrigList = htmlTable.getChildren("tr"); // can be empty
-
-                final List<Object> docxContents = sdtElement.getSdtContent().getContent();
-                final Tbl docxTbl = FirstMatch.matching(docxContents, Types.withType(Tbl.class));
-                if (docxTbl == null) {
-                    return false;
-                }
-                final List<Tr> docxTrList = AllMatches.matching(docxTbl, Types.withType(Tr.class));
-                if (docxTrList.size() < 2) {
-                    // require a header row and one other
-                    return false;
-                }
-
-                final List<Tr> docxTrNewList = Lists.newArrayList();
-                for (int htmlRowNum = 0; htmlRowNum < htmlTrOrigList.size(); htmlRowNum++) {
-                    final Element htmlTr = htmlTrOrigList.get(htmlRowNum);
-
-                    final int numDocxBodyTr = docxTrList.size() - 1;
-                    final int docxTrNum = (htmlRowNum % numDocxBodyTr) + 1;
-                    final Tr docxTr = XmlUtils.deepCopy(docxTrList.get(docxTrNum));
-                    docxTrNewList.add(docxTr);
-                    final List<Tc> docxTcList = AllMatches.matching(docxTr.getContent(), Types.withType(Tc.class));
-                    final List<Element> htmlTdList = htmlTr.getChildren("td");
-                    final List<String> htmlCellValues =
-                            htmlTdList.stream().map(x -> Jdom2.textValue().apply(x))
-                            .collect(Collectors.toList());
-                    for (int cellNum = 0; cellNum < docxTcList.size(); cellNum++) {
-                        final Tc docxTc = docxTcList.get(cellNum);
-                        final String value = cellNum < htmlCellValues.size() ? htmlCellValues.get(cellNum) : "";
-                        final P docxP = FirstMatch.matching(docxTc.getContent(), Types.withType(P.class));
-                        if (docxP == null) {
-                            return false;
-                        }
-                        final R docxR = FirstMatch.matching(docxP, Types.withType(R.class));
-                        if (docxR == null) {
-                            return false;
-                        }
-                        Docx.setText(docxR, value);
-                    }
-                }
-                docxReplaceRows(docxTbl, docxTrList, docxTrNewList);
-                return true;
-            }
-
-            private void docxReplaceRows(final Tbl docxTbl, final List<Tr> docxTrList, final List<Tr> docxTrToAdd) {
-                final List<Object> docxTblContent = docxTbl.getContent();
-                boolean first = true;
-                for (final Tr docxTr : docxTrList) {
-                    if (first) {
-                        // header, do NOT remove
-                        first = false;
-                    } else {
-                        docxTblContent.remove(docxTr);
-                    }
-                }
-                for (final Tr docxTr : docxTrToAdd) {
-                    docxTblContent.add(docxTr);
-                }
-            }
-        };
-
-        private final String type;
-
-
-        private MergeType(final String type) {
-            this.type = type;
-        }
-
-        public static MergeType lookup(final String name, final String clazz) {
-            final String type = name + (clazz != null ? "." + clazz : "");
-            for (final MergeType mt : values()) {
-                if (Objects.equal(mt.type, type)) {
-                    return mt;
-                }
-            }
-            return null;
-        }
-
-        boolean merge(final Element htmlElement, final SdtElement docxElement) {
-            final String htmlTextValue = Jdom2.textValueOf(htmlElement);
-            if (htmlTextValue == null) {
-                return false;
-            }
-
-            final R docxR = FirstMatch.matching(docxElement, Types.withType(R.class));
-            if (docxR == null) {
-                return false;
-            }
-            return Docx.setText(docxR, htmlTextValue);
-        }
-    }
-
-    private static void merge(final Element htmlBody, final Body docXBody, final MatchingPolicy matchingPolicy) throws MergeException {
-        final List<String> matchedInputIds = Lists.newArrayList();
-        final List<String> unmatchedInputIds = Lists.newArrayList();
-
-        final List<Content> htmlBodyContents = htmlBody.getContent();
-        for (final Content input : htmlBodyContents) {
-            if (!(input instanceof Element)) {
-                continue;
-            }
-            mergeInto((Element) input, docXBody, matchedInputIds, unmatchedInputIds);
-        }
-
-        final List<String> unmatchedPlaceHolders = unmatchedPlaceholders(docXBody, matchedInputIds);
-
-        matchingPolicy.unmatchedInputs(unmatchedInputIds);
-        matchingPolicy.unmatchedPlaceholders(unmatchedPlaceHolders);
-    }
-
-    private static void mergeInto(final Element input, final Body docXBody, final List<String> matchedInputs, final List<String> unmatchedInputs) throws MergeException {
-
-        final String id = Jdom2.attrOf(input, "id");
-        if (id == null) {
-            throw new MergeException("Missing 'id' attribute for element within body of input HTML");
-        }
-
-        final MergeType mergeType = MergeType.lookup(input.getName(), Jdom2.attrOf(input, "class"));
-        if (mergeType == null) {
-            unmatchedInputs.add(id);
-            return;
-        }
-
-        final SdtElement docxElement = FirstMatch.matching(docXBody, Docx.withTagVal(id));
-        if (docxElement == null) {
-            unmatchedInputs.add(id);
-            return;
-        }
-
-        if (mergeType.merge(input, docxElement)) {
-            matchedInputs.add(id);
-        } else {
-            unmatchedInputs.add(id);
-        }
-    }
-
-    private static List<String> unmatchedPlaceholders(final Body docXBody, final List<String> matchedIds) {
-        final List<SdtElement> taggedElements = AllMatches.matching(docXBody, Docx.withAnyTag());
-        final List<String> unmatchedPlaceHolders = taggedElements.stream().map(x -> Docx.tagToValue().apply(x)).collect(Collectors.toList());
-        unmatchedPlaceHolders.removeAll(matchedIds);
-        return unmatchedPlaceHolders;
-    }
-
-    private static File createTempFile() throws MergeException {
-        try {
-            return File.createTempFile("docx", null);
-        } catch (final IOException ex) {
-            throw new MergeException("Unable to create temporary working file", ex);
-        }
-    }
-}
+}
\ No newline at end of file
diff --git a/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/IsisModuleSubdomainsDocxApplib.java b/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/IsisModuleSubdomainsDocxApplib.java
index ce54e75..d12d16d 100644
--- a/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/IsisModuleSubdomainsDocxApplib.java
+++ b/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/IsisModuleSubdomainsDocxApplib.java
@@ -19,8 +19,14 @@
 package org.apache.isis.subdomains.docx.applib;
 
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+import org.apache.isis.subdomains.docx.applib.services.DocxServiceDefault;
 
 @Configuration
+@Import({
+    DocxServiceDefault.class
+})
 public class IsisModuleSubdomainsDocxApplib {
 
 }
diff --git a/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/DocxService.java b/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/services/DocxServiceDefault.java
similarity index 76%
copy from subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/DocxService.java
copy to subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/services/DocxServiceDefault.java
index 3d1a25a..dd3fda8 100644
--- a/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/DocxService.java
+++ b/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/services/DocxServiceDefault.java
@@ -16,7 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.isis.subdomains.docx.applib;
+package org.apache.isis.subdomains.docx.applib.services;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -28,6 +28,9 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
 
+import javax.annotation.Priority;
+import javax.inject.Named;
+
 import org.apache.commons.compress.utils.Lists;
 import org.apache.commons.io.IOUtils;
 import org.docx4j.Docx4J;
@@ -48,9 +51,12 @@ import org.docx4j.wml.Tr;
 import org.jdom2.Content;
 import org.jdom2.Element;
 import org.jdom2.input.DOMBuilder;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 import org.w3c.dom.Document;
 
+import org.apache.isis.applib.annotation.PriorityPrecedence;
+import org.apache.isis.subdomains.docx.applib.DocxService;
 import org.apache.isis.subdomains.docx.applib.exceptions.LoadInputException;
 import org.apache.isis.subdomains.docx.applib.exceptions.LoadTemplateException;
 import org.apache.isis.subdomains.docx.applib.exceptions.MergeException;
@@ -60,107 +66,15 @@ import org.apache.isis.subdomains.docx.applib.util.Docx;
 import org.apache.isis.subdomains.docx.applib.util.Jdom2;
 import org.apache.isis.subdomains.docx.applib.util.Types;
 
-import lombok.Builder;
-import lombok.Getter;
 import lombok.val;
 
-/**
- * Provides a mail-merge capability.
- *
- * @since 2.x {@index}
- */
 @Service
-public class DocxService {
-
-    /**
-     * @since 2.x {@index}
-     */
-    @Getter
-    @Builder(builderClassName = "Builder")
-    public static class MergeParams {
-
-        /**
-         * Defines the policy for matching input to placeholders.
-         *
-         * <p>
-         *     Does not need to be specified, will default to {@link MatchingPolicy#STRICT strict}
-         * </p>
-         */
-        @lombok.Builder.Default private MatchingPolicy matchingPolicy = MatchingPolicy.STRICT;
-
-        /**
-         * Defines whether to output as Word docx or PDF.
-         *
-         * <p>
-         *     Does not need to be specified, will default to {@link OutputType#DOCX docx}.
-         * </p>
-         */
-        @lombok.Builder.Default private OutputType outputType = OutputType.DOCX;
-
-        /**
-         * Holds the input arguments to be merged into the template.
-         *
-         * <p>
-         * Either this or {@link #getInputAsHtmlDoc()} must be specified.
-         * Preference is given to {@link #getInputAsHtmlDoc()}.
-         * </p>
-         *
-         * @see #getInputAsHtmlDoc()
-         */
-        private String inputAsHtml;
-
-        /**
-         * Holds the input arguments to be merged into the template.
-         *
-         * <p>
-         * Either this or {@link #getInputAsHtml()} must be specified, with
-         * preference given to this.
-         * </p>
-         *
-         * @see #getInputAsHtml()
-         */
-        private org.w3c.dom.Document inputAsHtmlDoc;
-
-        /**
-         * Refers to the template with place holders to be merged into.
-         *
-         * <p>
-         *     Either this or {@link #getDocxTemplateAsWpMlPackage()} myst be
-         *     specified, with preference given to {@link #getDocxTemplateAsWpMlPackage()}
-         * </p>
-         *
-         * @see #getDocxTemplateAsWpMlPackage()
-         */
-        private InputStream docxTemplate;
-
-        /**
-         * Refers to the template with place holders to be merged into.
-         *
-         * <p>
-         *     Either this or {@link #getDocxTemplate()} myst be
-         *     specified, with preference given to this.
-         * </p>
-         *
-         * @see #getDocxTemplate()
-         */
-        private WordprocessingMLPackage docxTemplateAsWpMlPackage;
-
-        /**
-         * The output stream to write to.
-         */
-        private OutputStream output;
-
-    }
+@Named("isis.sub.docx.ClockService")
+@Priority(PriorityPrecedence.MIDPOINT)
+@Qualifier("Default")
+public class DocxServiceDefault implements DocxService {
 
-    /**
-     * Load and return an in-memory representation of a docx.
-     *
-     * <p>
-     * This is public API because building the in-memory structure can be
-     * quite slow.  Thus, clients can use this method to cache the in-memory
-     * structure, and pass it in the {@link MergeParams} (through the
-     * {@link MergeParams.Builder#docxTemplateAsWpMlPackage(WordprocessingMLPackage) builder method})
-     */
+    @Override
     public WordprocessingMLPackage loadPackage(final InputStream docxTemplate) throws LoadTemplateException {
         final WordprocessingMLPackage docxPkg;
         try {
@@ -171,9 +85,7 @@ public class DocxService {
         return docxPkg;
     }
 
-    /**
-     * Merge the input arguments (as HTML) against the Docx template, writing out as a Word docx..
-     */
+    @Override
     public void merge(final MergeParams mergeDefn) throws LoadInputException, LoadTemplateException, MergeException {
 
         final org.jdom2.Document htmlJdomDoc;
@@ -208,6 +120,8 @@ public class DocxService {
         merge(htmlJdomDoc, docxPkg, output, mergeDefn.getMatchingPolicy(), defensiveCopy, mergeDefn.getOutputType());
     }
 
+    // -- HELPER
+
     private void merge(
             final org.jdom2.Document htmlDoc,
             final WordprocessingMLPackage docxTemplateInput,
@@ -232,7 +146,7 @@ public class DocxService {
 
 
                 final FOSettings foSettings = Docx4J.createFOSettings();
-                foSettings.setWmlPackage(docxTemplate);
+                foSettings.setOpcPackage(docxTemplate);
 
                 try {
                     final Mapper fontMapper = new IdentityPlusMapper();
@@ -269,55 +183,11 @@ public class DocxService {
         }
     }
 
-    /**
-     * Defines the strategy as to whether placeholders must exactly input data
-     * (or whether there can be unmatched placeholders, or conversely unused input data).
-     */
-    public enum MatchingPolicy {
-        STRICT(false, false),
-        ALLOW_UNMATCHED_INPUT(true, false),
-        ALLOW_UNMATCHED_PLACEHOLDERS(false, true),
-        /**
-         * Combination of both {@link #ALLOW_UNMATCHED_INPUT} and {@link #ALLOW_UNMATCHED_PLACEHOLDERS}.
-         */
-        LAX(true, true);
-        private final boolean allowUnmatchedInput;
-        private final boolean allowUnmatchedPlaceholders;
-
-        private MatchingPolicy(final boolean allowUnmatchedInput, final boolean allowUnmatchedPlaceholders) {
-            this.allowUnmatchedInput = allowUnmatchedInput;
-            this.allowUnmatchedPlaceholders = allowUnmatchedPlaceholders;
-        }
-
-        public void unmatchedInputs(final List<String> unmatched) throws MergeException {
-            if (!allowUnmatchedInput && !unmatched.isEmpty()) {
-                throw new MergeException("Input elements " + unmatched + " were not matched to placeholders");
-            }
-        }
-
-        public void unmatchedPlaceholders(final List<String> unmatched) throws MergeException {
-            if (!allowUnmatchedPlaceholders && !unmatched.isEmpty()) {
-                throw new MergeException("Placeholders " + unmatched + " were not matched to input");
-            }
-        }
-    }
-
     private enum DefensiveCopy {
         REQUIRED,
         NOT_REQUIRED
     }
 
-    /**
-     * The type of the file to generate
-     */
-    public enum OutputType {
-        DOCX,
-        /**
-         * Support for PDF should be considered experimental.
-         */
-        PDF
-    }
-
     private enum MergeType {
         PLAIN("p.plain"),
         RICH("p.rich"),
diff --git a/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Docx.java b/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Docx.java
index e979c95..cd37671 100644
--- a/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Docx.java
+++ b/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Docx.java
@@ -44,11 +44,11 @@ import lombok.experimental.UtilityClass;
 @UtilityClass
 public class Docx {
 
-    public static Function<SdtElement, String> tagToValue() {
+    public Function<SdtElement, String> tagToValue() {
         return input -> input.getSdtPr().getTag().getVal();
     }
 
-    public static Predicate<Object> withAnyTag() {
+    public Predicate<Object> withAnyTag() {
         return object -> {
             if(!(object instanceof SdtElement)) {
                 return false;
@@ -59,7 +59,7 @@ public class Docx {
         };
     }
 
-    public static Predicate<Object> withTagVal(final String tagVal) {
+    public Predicate<Object> withTagVal(final String tagVal) {
         return object -> {
             if(!(object instanceof SdtElement)) {
                 return false;
@@ -70,9 +70,8 @@ public class Docx {
         };
     }
 
-    @SuppressWarnings({ "rawtypes", "restriction" })
-    public
-    static boolean setText(R run, String value) {
+    @SuppressWarnings({ "rawtypes" })
+    public boolean setText(final R run, final String value) {
         List<Object> runContent = run.getContent();
         if(runContent.isEmpty()) {
             return false;
@@ -92,13 +91,13 @@ public class Docx {
         return true;
     }
 
-    public static Body docxBodyFor(WordprocessingMLPackage docxPkg) {
+    public Body docxBodyFor(final WordprocessingMLPackage docxPkg) {
         val docxMdp = docxPkg.getMainDocumentPart();
         val docxDoc = docxMdp.getJaxbElement();
         return docxDoc.getBody();
     }
 
-    public static WordprocessingMLPackage clone(WordprocessingMLPackage docxTemplate) throws MergeException {
+    public WordprocessingMLPackage clone(WordprocessingMLPackage docxTemplate) throws MergeException {
         val foxc = new FlatOpcXmlCreator(docxTemplate);
         val baos = new ByteArrayOutputStream();
         try {
@@ -108,7 +107,7 @@ public class Docx {
             docxTemplate = (WordprocessingMLPackage) foxi.get();
         } catch (Docx4JException e) {
             throw new MergeException("unable to defensive copy (problem exporting)", e);
-        } catch (@SuppressWarnings("restriction") JAXBException e) {
+        } catch (JAXBException e) {
             throw new MergeException("unable to defensive copy (problem importing)", e);
         }
         return docxTemplate;
diff --git a/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Dump.java b/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Dump.java
index 80ee728..83636e2 100644
--- a/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Dump.java
+++ b/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Dump.java
@@ -39,7 +39,7 @@ import org.docx4j.wml.Body;
 
 public class Dump {
 
-    public static void main(String[] args) throws Exception {
+    public static void main(final String[] args) throws Exception {
 
         //String filename = "helloWorld.docx";
         String filename = "TypicalDocument.docx";
@@ -60,11 +60,11 @@ public class Dump {
 
     private Map<Part, Part> handled = new HashMap<Part, Part>();
 
-    public Dump(File file) {
+    public Dump(final File file) {
         this.file = file;
     }
 
-    public void partsList(PrintStream out) throws Exception {
+    public void partsList(final PrintStream out) throws Exception {
 
         OpcPackage opcPackage = OpcPackage.load(file);
 
@@ -81,8 +81,8 @@ public class Dump {
         // saver.save(System.getProperty("user.dir") + "/out.docx");
     }
 
-    @SuppressWarnings({ "restriction", "rawtypes" })
-    private void appendInfo(Part p, StringBuilder sb, String indent) {
+    @SuppressWarnings({ "rawtypes" })
+    private void appendInfo(final Part p, final StringBuilder sb, final String indent) {
 
         String relationshipType = "";
         if (p.getSourceRelationships().size() > 0) {
@@ -101,7 +101,7 @@ public class Dump {
         }
     }
 
-    private void traverseRelationships(OpcPackage opcPackage, RelationshipsPart rp, StringBuilder sb, String indent) {
+    private void traverseRelationships(final OpcPackage opcPackage, final RelationshipsPart rp, final StringBuilder sb, final String indent) {
 
         // TODO: order by rel id
 
@@ -134,11 +134,11 @@ public class Dump {
         }
     }
 
-    public void documentTraverse(PrintStream out) throws Docx4JException {
+    public void documentTraverse(final PrintStream out) throws Docx4JException {
         WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.load(file);
         MainDocumentPart documentPart = wordMLPackage.getMainDocumentPart();
 
-        org.docx4j.wml.Document wmlDocumentEl = (org.docx4j.wml.Document) documentPart.getJaxbElement();
+        org.docx4j.wml.Document wmlDocumentEl = documentPart.getJaxbElement();
         Body body = wmlDocumentEl.getBody();
 
         new TraversalUtil(body,
@@ -147,7 +147,8 @@ public class Dump {
 
                 String indent = "";
 
-                public List<Object> apply(Object o) {
+                @Override
+                public List<Object> apply(final Object o) {
 
                     String text = "";
                     if (o instanceof org.docx4j.wml.Text)
@@ -157,12 +158,14 @@ public class Dump {
                     return null;
                 }
 
-                public boolean shouldTraverse(Object o) {
+                @Override
+                public boolean shouldTraverse(final Object o) {
                     return true;
                 }
 
                 // Depth first
-                public void walkJAXBElements(Object parent) {
+                @Override
+                public void walkJAXBElements(final Object parent) {
 
                     indent += "    ";
 
@@ -187,7 +190,8 @@ public class Dump {
                     indent = indent.substring(0, indent.length() - 4);
                 }
 
-                public List<Object> getChildren(Object o) {
+                @Override
+                public List<Object> getChildren(final Object o) {
                     return TraversalUtil.getChildrenImpl(o);
                 }
             }
diff --git a/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Jdom2.java b/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Jdom2.java
index aa6ee92..a1bf6a7 100644
--- a/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Jdom2.java
+++ b/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Jdom2.java
@@ -34,11 +34,12 @@ import org.jdom2.input.SAXBuilder;
 import org.apache.isis.subdomains.docx.applib.exceptions.LoadInputException;
 import org.apache.isis.subdomains.docx.applib.exceptions.MergeException;
 
-public final class Jdom2 {
+import lombok.experimental.UtilityClass;
 
-    private Jdom2(){}
+@UtilityClass
+public final class Jdom2 {
 
-    public static String textValueOf(Element htmlElement) {
+    public String textValueOf(final Element htmlElement) {
         List<Content> htmlContent = htmlElement.getContent();
         if(htmlContent.isEmpty()) {
             return null;
@@ -52,19 +53,20 @@ public final class Jdom2 {
     }
 
 
-    private static String normalized(String value) {
+    private String normalized(final String value) {
         String replaceAll = value.replaceAll("\\s+", " ");
         return replaceAll;
     }
 
-    public static Function<Element, String> textValue() {
+    public Function<Element, String> textValue() {
         return  new Function<Element, String>(){
-        public String apply(Element input) {
+        @Override
+        public String apply(final Element input) {
             return textValueOf(input);
         }};
     }
 
-    public static String attrOf(Element input, String attname) {
+    public String attrOf(final Element input, final String attname) {
         Attribute attribute = input.getAttribute(attname);
         if(attribute == null) {
             return null;
@@ -72,7 +74,7 @@ public final class Jdom2 {
         return attribute.getValue();
     }
 
-    public static Document loadInput(String html) throws LoadInputException {
+    public Document loadInput(final String html) throws LoadInputException {
         try {
             return new SAXBuilder().build(new StringReader(html));
         } catch (JDOMException e) {
@@ -82,7 +84,7 @@ public final class Jdom2 {
         }
     }
 
-    public static Element htmlBodyFor(Document htmlDoc) throws MergeException {
+    public Element htmlBodyFor(final Document htmlDoc) throws MergeException {
         Element htmlEl = htmlDoc.getRootElement();
         Element bodyEl = htmlEl.getChild("body");
         if (bodyEl == null) {
diff --git a/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Types.java b/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Types.java
index 5447ee4..91ad2bc 100644
--- a/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Types.java
+++ b/subdomains/docx/applib/src/main/java/org/apache/isis/subdomains/docx/applib/util/Types.java
@@ -25,7 +25,7 @@ import lombok.experimental.UtilityClass;
 @UtilityClass
 public final class Types {
 
-    public static Predicate<Object> withType(final Class<?> cls) {
+    public Predicate<Object> withType(final Class<?> cls) {
         return object -> cls.isAssignableFrom(object.getClass());
     }
 
diff --git a/subdomains/docx/applib/src/test/java/org/apache/isis/subdomains/docx/applib/DocxService_merge_Test.java b/subdomains/docx/applib/src/test/java/org/apache/isis/subdomains/docx/applib/DocxService_merge_Test.java
index 1777578..1f51a8e 100644
--- a/subdomains/docx/applib/src/test/java/org/apache/isis/subdomains/docx/applib/DocxService_merge_Test.java
+++ b/subdomains/docx/applib/src/test/java/org/apache/isis/subdomains/docx/applib/DocxService_merge_Test.java
@@ -33,6 +33,7 @@ import static org.assertj.core.api.Assumptions.assumeThat;
 
 import org.apache.isis.subdomains.docx.applib.exceptions.LoadInputException;
 import org.apache.isis.subdomains.docx.applib.exceptions.MergeException;
+import org.apache.isis.subdomains.docx.applib.services.DocxServiceDefault;
 
 import lombok.val;
 
@@ -45,7 +46,7 @@ class DocxService_merge_Test {
 
     @BeforeEach
     public void setUp() throws Exception {
-        docxService = new DocxService();
+        docxService = new DocxServiceDefault();
 
         // given
         docxTemplate = docxService.loadPackage(io.openInputStream("Template.docx"));
@@ -55,7 +56,7 @@ class DocxService_merge_Test {
     @Nested
     public class Strict {
 
-        private DocxService.MatchingPolicy matchingPolicy = DocxService.MatchingPolicy.STRICT;
+        private DocxServiceDefault.MatchingPolicy matchingPolicy = DocxService.MatchingPolicy.STRICT;
 
         @Test
         public void exactMatch() throws Exception {
@@ -129,7 +130,7 @@ class DocxService_merge_Test {
     @Nested
     public class AllowUnmatchedInput {
 
-        private DocxService.MatchingPolicy matchingPolicy = DocxService.MatchingPolicy.ALLOW_UNMATCHED_INPUT;
+        private DocxServiceDefault.MatchingPolicy matchingPolicy = DocxService.MatchingPolicy.ALLOW_UNMATCHED_INPUT;
 
         @Test
         public void exactMatch() throws Exception {
@@ -186,7 +187,7 @@ class DocxService_merge_Test {
     @Nested
     public class AllowUnmatchedPlaceholders {
 
-        private DocxService.MatchingPolicy matchingPolicy = DocxService.MatchingPolicy.ALLOW_UNMATCHED_PLACEHOLDERS;
+        private DocxServiceDefault.MatchingPolicy matchingPolicy = DocxService.MatchingPolicy.ALLOW_UNMATCHED_PLACEHOLDERS;
 
         @Test
         public void exactMatch() throws Exception {
@@ -242,7 +243,7 @@ class DocxService_merge_Test {
     @Nested
     public class Lax {
 
-        private DocxService.MatchingPolicy matchingPolicy = DocxService.MatchingPolicy.LAX;
+        private DocxServiceDefault.MatchingPolicy matchingPolicy = DocxService.MatchingPolicy.LAX;
 
         @Test
         public void exactMatch() throws Exception {
@@ -322,7 +323,7 @@ class DocxService_merge_Test {
     @Nested
     public class GeneratePdf {
 
-        private DocxService.MatchingPolicy matchingPolicy = DocxService.MatchingPolicy.STRICT;
+        private DocxServiceDefault.MatchingPolicy matchingPolicy = DocxService.MatchingPolicy.STRICT;
 
         @BeforeEach
         public void setUp() throws Exception {