wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

By Date: March 2015

XPages XML Document DataSource - Take 2


For a recent project I revisited the idea of storing XML documents as MIME entries in Notes - while preserving some of the fields for use in views and the Notes client. Jesse suggested I should have a look at annotations. Turns out, it is easier that it sound. To create an annotation that works at runtime, I need a one liner only:
@Retention(RetentionPolicy.RUNTIME) public @interface ItemPathMappings { String[] value(); }
To further improve usefulness, I created a "BaseConfiguration" my classes will inherit from, that contains the common properties I want all my classes (and documents) to have. You might want to adjust it to your needs:

ackage com.notessensei.domino;
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
 * Common methods implemented by all classes to be Dominoserialized
 */
@XmlRootElement(name = "BaseConfiguration")
@XmlAccessorType(XmlAccessType.NONE)
public abstract class BaseConfiguration implements Serializable, Comparable<BaseConfiguration> {
    private static final long serialVersionUID = 1L;
    @XmlAttribute(name = "name")
    protected String          name;

    public int compareTo(BaseConfiguration bc) {
        return this.toString().compareTo(bc.toString());
    }
    public String getName() {
        return this.name;
    }
    public BaseConfiguration setName(String name) {
        this.name = name;
        return this;
    }
    @Override
    public String toString() {
        return Serializer.toJSONString(this);
    }
    public String toXml() {
        return Serializer.toXMLString(this);
    }
}

The next building block is my Serializer support with a couple of static methods, that make dealing with XML and JSON easier.

package com.notessensei.domino;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

/**
 * Helper class to serialize / deserialize from/to JSON and XML
 */
public class Serializer {

    public static String toJSONString(Object o) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            Serializer.saveJSON(o, out);
        } catch (IOException e) {
            return e.getMessage();
        }
        return out.toString();
    }

    public static String toXMLString(Object o) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            Serializer.saveXML(o, out);
        } catch (Exception e) {
            return e.getMessage();
        }
        return out.toString();
    }

    public static void saveJSON(Object o, OutputStream out) throws IOException {
        GsonBuilder gb = new GsonBuilder();
        gb.setPrettyPrinting();
        gb.disableHtmlEscaping();
        Gson gson = gb.create();
        PrintWriter writer = new PrintWriter(out);
        gson.toJson(o, writer);
        writer.flush();
        writer.close();
    }

    public static void saveXML(Object o, OutputStream out) throws Exception {
        JAXBContext context = JAXBContext.newInstance(o.getClass());
        Marshaller m = context.createMarshaller();
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        m.marshal(o, out);
    }

    public static org.w3c.dom.Document getDocument(Object source) throws ParserConfigurationException, JAXBException {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        org.w3c.dom.Document doc = db.newDocument();
        JAXBContext context = JAXBContext.newInstance(source.getClass());
        Marshaller m = context.createMarshaller();
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        m.marshal(source, doc);
        return doc;
    }

    @SuppressWarnings("rawtypes")
    public static Object fromByte(byte[] source, Class targetClass) throws JAXBException {
        ByteArrayInputStream in = new ByteArrayInputStream(source);
        JAXBContext context = JAXBContext.newInstance(targetClass);
        Unmarshaller um = context.createUnmarshaller();
        return targetClass.cast(um.unmarshal(in));
    }
}

The key piece is for the XML serialization/deserialization to work is the abstract class AbstractXmlDocument. That class contains the load and save methods that interact with Domino's MIME capabilities as well as executing the XPath expressions to store the Notes fields. The implementations of this abstract class will have annotations that combine the Notes field name, the type and the XPath expression. An implementation would look like this:

package com.notessensei.domino.xmldocument;
import javax.xml.bind.JAXBException;
import lotus.domino.Database;
import lotus.domino.Document;
import lotus.domino.NotesException;
import lotus.domino.Session;
import com.notessensei.domino.ApplicationConfiguration;
import com.notessensei.domino.Serializer;
import com.notessensei.domino.xmldocument.AbstractXmlDocument.ItemPathMappings;

// The ItemPathMappings are application specific!
@ItemPathMappings({ "Subject|Text|/Application/@name",
     "Description|Text|/Application/description",
     "Unid|Text|/Application/@unid",
     "Audience|Text|/Application/Audiences/Audience",
     "NumberOfViews|Number|count(/Application/Views/View)",
     "NumberOfForms|Number|count(/Application/Forms/Form)",
     "NumberOfColumns|Number|count(/Application/Views/View/columns/column)",
     "NumberOfFields|Number|count(/Application/Forms/Form/fields/field)",
     "NumberOfActions|Number|count(//action)" })
public class ApplicationXmlDocument extends AbstractXmlDocument {

    public ApplicationXmlDocument(String formName) {
        super(formName);
    }

    @SuppressWarnings("unchecked")
    @Override
    public ApplicationConfiguration load(Session session, Document d) {

        ApplicationConfiguration result = null;
        try {
            result = (ApplicationConfiguration) Serializer.fromByte(this.loadFromMime(session, d), ApplicationConfiguration.class);
        } catch (JAXBException e) {
            e.printStackTrace();
        }
        try {
            result.setUnid(d.getUniversalID());
        } catch (NotesException e) {
            // No Action Taken
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    @Override
    public ApplicationConfiguration load(Session session, Database db, String unid) {
        Document doc;
        try {
            doc = db.getDocumentByUNID(unid);
            if (doc != null) {
                ApplicationConfiguration result = this.load(session, doc);
                doc.recycle();
                return result;
            }

        } catch (NotesException e) {
            e.printStackTrace();
        }

        return null;
    }
}
##
Here is the base class:

package com.notessensei.domino.xmldocument;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Scanner;
import java.util.TreeMap;

import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import lotus.domino.Item;
import lotus.domino.MIMEEntity;
import lotus.domino.MIMEHeader;
import lotus.domino.NotesException;
import lotus.domino.Stream;

import org.w3c.dom.Document;
import org.xml.sax.InputSource;

import com.notessensei.domino.BaseConfiguration;
import com.notessensei.domino.Serializer;

@XmlAccessorType(XmlAccessType.NONE)
public abstract class AbstractXmlDocument {

    /**
     * Holds the the definition that extracts data from an XML Document into
     * a Notes Item. Will be triggered by an annotation in the implementation class
     */
    public class FieldExtractDefinition implements Comparable<FieldExtractDefinition> {

        public final static String ELEMENT_SEPARATOR = "\\|";
        private final String       path;
        private final String       itemName;
        private final FieldType    fieldType;

        /**
         * Creates a FieldExtractDefinition from a raw String in the format:
         * Itemname|FieldType|XPath
         *
         * @param rawString
         */
        public FieldExtractDefinition(String rawString) {
            Scanner scanner = new Scanner(rawString);
            scanner.useDelimiter(ELEMENT_SEPARATOR);
            this.itemName = scanner.next();
            this.fieldType = FieldType.valueOf(scanner.next().toUpperCase());
            // The rest of the elements make the XPath
            this.path = scanner.nextLine().substring(1);
        }

        public int compareTo(FieldExtractDefinition fed) {
            return this.toString().compareTo(fed.toString());
        }

        public FieldType getDataType() {
            return fieldType;
        }

        public String getItemName() {
            return itemName;
        }

        public String getXPath() {
            return path;
        }

        @Override
        public String toString() {
            return Serializer.toJSONString(this);
        }

    }

    public enum FieldType {
        NUMBER, TEXT, NAMES, READER, AUTHOR, DATE
    }

    @Retention(RetentionPolicy.RUNTIME)
    public @interface ItemPathMappings {

        String[] value();

    }

    private final String                             MIME_FIELD_NAME = "Body";
    private final String                             FORM_NAME       = "Form";

    private final Collection<FieldExtractDefinition> definitions     = new HashSet<FieldExtractDefinition>();

    private String                                   formName;

    private org.w3c.dom.Document                     style           = null;

    public AbstractXmlDocument(String formName) {
        this.formName = formName;
        // Now load the annotations
        ItemPathMappings ipm = this.getClass().getAnnotation(ItemPathMappings.class);
        if (ipm != null) {
            for (String oneMapping : ipm.value()) {
                this.definitions.add(new FieldExtractDefinition(oneMapping));
            }
        }
    }

    public String getFormName() {
        return this.formName;
    }

    public org.w3c.dom.Document getStyle() {
        return this.style;
    }

    public abstract <T extends BaseConfiguration> T load(lotus.domino.Session session, lotus.domino.Database db, String unid);

    public abstract <T extends BaseConfiguration> T load(lotus.domino.Session session, lotus.domino.Document d);

    public boolean save(lotus.domino.Session session, lotus.domino.Document d, Object source) {
        boolean success = true;
        try {
            boolean oldMime = session.isConvertMime();
            session.setConvertMime(false);

            // Save the form to be sure
            if (this.formName != null) {
                d.replaceItemValue(FORM_NAME, this.formName);
            }

            // We must remove the mime body - otherwise it will not work
            if (d.hasItem(MIME_FIELD_NAME)) {
                d.removeItem(MIME_FIELD_NAME);
            }

            org.w3c.dom.Document domDoc = Serializer.getDocument(source);

            // Create the top mime entry
            MIMEEntity root = d.createMIMEEntity(MIME_FIELD_NAME);
            MIMEHeader header = root.createHeader("Content-Type");
            header.setHeaderVal("multipart/mixed");

            MIMEEntity body = root.createChildEntity();
            MIMEHeader bheader = body.createHeader("Content-Type");
            bheader.setHeaderVal("multipart/alternative");

            MIMEEntity textMime = body.createChildEntity();
            MIMEHeader textHeader = textMime.createHeader("Content-Type");
            String cType = (this.style == null) ? "text/plain" : "text/html";
            textHeader.setHeaderVal(cType);

            Stream stream2 = session.createStream();
            stream2.write(this.dom2Byte(domDoc, this.style));
            textMime.setContentFromBytes(stream2, cType + "; charset=\"UTF-8\"", MIMEEntity.ENC_NONE);

            MIMEEntity xmlMime = body.createChildEntity();
            MIMEHeader xmlHeader = xmlMime.createHeader("Content-Type");
            xmlHeader.setHeaderVal("application/xml");

            Stream stream = session.createStream();
            stream.write(this.dom2Byte(domDoc, null));
            xmlMime.setContentFromBytes(stream, "application/xml; charset=\"UTF-8\"", MIMEEntity.ENC_NONE);

            // Now extract the fields
            this.extractFields(domDoc, d, body);

            session.setConvertMime(oldMime);
        } catch (NotesException e) {
            success = false;
            e.printStackTrace();
        } catch (ParserConfigurationException e) {
            success = false;
            e.printStackTrace();
        } catch (JAXBException e) {
            success = false;
            e.printStackTrace();
        }

        return success;
    }

    public Map<String, String> extractMappingResults(Object source) throws ParserConfigurationException, JAXBException {
        Map<String, String> result = new TreeMap<String, String>();
        org.w3c.dom.Document domDoc = (source instanceof org.w3c.dom.Document) ? (org.w3c.dom.Document) source : Serializer
                    .getDocument(source);
        for (FieldExtractDefinition f : this.definitions) {
            String candidate = this.extractOneField(domDoc, f.getXPath());
            if (candidate != null) {
                result.put(f.getItemName(), candidate);
            }
        }

        return result;
    }

    public void setFormName(String formName) {
        this.formName = formName;
    }

    public AbstractXmlDocument setPrettyPrintStyle(org.w3c.dom.Document style) {
        this.style = style;
        return this;
    }

    public void setStyle(InputStream styleStream) {

        Document d = null;
        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(false); // Will blow if set to true
        factory.setNamespaceAware(true);

        try {
            InputSource source = new InputSource(styleStream);
            final DocumentBuilder docb = factory.newDocumentBuilder();
            d = docb.parse(source);

        } catch (final Exception e) {
            e.printStackTrace();
            d = null;
        }

        if (d == null) {
            System.out.println("DOM from stream generation failed:\n");
        }

        this.style = d;
    }

    /**
     * Loads XML from Notes document and returns the content as bytes to
     * be marshalled into a Java object creation
     *
     * @param session
     *            NotesSession
     * @param d
     *            Document with MIME content
     * @return bytes for object instance creation
     */
    protected byte[] loadFromMime(lotus.domino.Session session, lotus.domino.Document d) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        try {
            boolean oldMime = session.isConvertMime();
            session.setConvertMime(false);

            // Load the TOP mime entitiy - must be multipart/mixed
            MIMEEntity root = d.getMIMEEntity(MIME_FIELD_NAME);
            if (root != null && root.getContentType().equals("multipart") && root.getContentSubType().equals("mixed")) {
                // We check for the body whtou should be multipart/alternate
                MIMEEntity body = root.getFirstChildEntity();
                if (body != null && body.getContentType().equals("multipart") && body.getContentSubType().equals("alternative")) {
                    // Now we go after Content-Type = application/xml
                    MIMEEntity payload = body.getFirstChildEntity();

                    while (payload != null && !payload.getContentSubType().equals("xml")) {
                        System.out.println(payload.getContentType());
                        System.out.println(payload.getContentSubType());
                        MIMEEntity nextMime = payload.getNextEntity();
                        payload.recycle();
                        payload = nextMime;
                    }

                    if (payload != null) {
                        Stream notesStream = session.createStream();
                        payload.getContentAsBytes(notesStream);
                        notesStream.setPosition(0);
                        notesStream.getContents(out);
                        notesStream.recycle();
                    }
                }

            }
            ;
            session.setConvertMime(oldMime);
        } catch (NotesException e) {
            e.printStackTrace();
        }

        return out.toByteArray();
    }

    private byte[] dom2Byte(Document dom, Document stylesheet) {
        StreamResult xResult = null;
        DOMSource source = null;
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        Transformer transformer = null;

        try {
            final TransformerFactory tFactory = TransformerFactory.newInstance();
            xResult = new StreamResult(out);
            source = new DOMSource(dom);

            if (stylesheet != null) {
                final DOMSource style = new DOMSource(stylesheet);
                transformer = tFactory.newTransformer(style);
            } else {
                transformer = tFactory.newTransformer();
            }

            // We don't want the XML declaration in front
            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");

            transformer.transform(source, xResult);

        } catch (final Exception e) {
            e.printStackTrace();
        }

        return out.toByteArray();

    }

    private void extractFields(Document domDoc, lotus.domino.Document d, MIMEEntity body) {
        Map<String, String> itemList = null;
        try {
            itemList = this.extractMappingResults(domDoc);
        } catch (ParserConfigurationException e1) {
            e1.printStackTrace();
        } catch (JAXBException e1) {
            e1.printStackTrace();
        }

        if (itemList == null) {
            return;
        }

        for (FieldExtractDefinition f : this.definitions) {

            String fName = f.getItemName();
            String fValue = itemList.get(fName);

            if (!fName.equalsIgnoreCase(MIME_FIELD_NAME) && !(fValue == null)) {
                // We can't allow to overwrite the body or write empty values
                try {
                    MIMEHeader h = body.createHeader(fName);
                    h.setHeaderVal(fValue);
                    Item curItem = d.replaceItemValue(fName, fValue);
                    FieldType ft = f.getDataType();
                    if (ft.equals(FieldType.AUTHOR)) {
                        curItem.setAuthors(true);
                    } else if (ft.equals(FieldType.READER)) {
                        curItem.setReaders(true);
                    }

                } catch (NotesException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    private String extractOneField(Document domDoc, String xPathString) {

        Object exprResult = null;
        final XPath xpath = XPathFactory.newInstance().newXPath();
        final MagicNamespaceContext nsc = new MagicNamespaceContext();
        xpath.setNamespaceContext(nsc);

        try {
            exprResult = xpath.evaluate(xPathString, domDoc);
        } catch (final XPathExpressionException e) {
            System.err.println("XPATH failed for " + xPathString);
            System.err.println(e.getMessage());
        }
        // if it failed we abort
        if (exprResult == null) {
            return null;
        }

        return exprResult.toString();

    }
}

As time permits I will provide a full working example.
YMMV!

Posted by on 05 March 2015 | Comments (0) | categories: XPages

Develop local, deploy (cloud) global - Java and CouchDB


Leaving the cosy world of Domino Designer behind, venturing into IBM Bluemix, Java and Cloudant, I'm challenged with a new set of task to master. Spoiled by Notes where Ctrl+O gives you instant access to any application, regardless of being stored locally or on a server I struggled a little with my usual practise of

develop local, deploy (Bluemix) global

The task at hand is to develop a Java Liberty based application, that uses CouchDB/Cloudant as its NoSQL data store. I want to be able to develop/test the application while being completely offline and deploy it to Bluemix. I don't want any code to have conditions offline/online, but rather use configuration of the runtimes for it.
Luckily I have access to really smart developers (thx Sai), so I succeeded.
This is what I found out, I needed to do. The list serves as reference for myself and others living in a latency/bandwidth challenged environment.
  1. Read: There are a number of articles around, that contain bits and pieces of the information required. In no specific order:
  2. Install: This is a big jump forward. No more looking for older versions, but rather bleeding edge. Tools of the trade:
    • GIT. When you are on Windows or Mac, try the nice GUI of SourceTree, and don't forget to learn git-flow (best explained here)
    • A current version of the Eclipse IDE (Luna at the time of writing, the Java edition suffices)
    • The liberty profile beta. The Beta is necessary, since it contains some of the features, notably couchdb, which are available in Bluemix by default. Use the option to drag the link onto your running Eclipse client
    • Maven - the Java way to resolve dependencies (guess where bower and npm got their ideas from)
    • CURL (that's my little command line ninja stuff, you can get away without it)
    • Apache CouchDB
  3. Configure: Java loves indirection. So there are a few moving parts as well (details below)
    • The Cloudant service in Bluemix
    • The JNDI name in the web.xml. Bluemix will discover the Cloudant service and create the matching entries in the server.xml automagically
    • A local profile for a server running the Liberty 9.0 profile
    • The configuration for the local CouchDB in the local server.xml
    • Replication between your local CouchDB instance and the Cloudant server database (if you want to keep the data in sync)
The flow of the data access looks like this
Develop local, deploy global

Read more

Posted by on 02 March 2015 | Comments (3) | categories: Bluemix CouchDB Java