org.openmrs.projectbuendia.webservices.rest.XformResource.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.projectbuendia.webservices.rest.XformResource.java

Source

// Copyright 2015 The Project Buendia Authors
//
// 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 distrib-
// uted 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
// specific language governing permissions and limitations under the License.

package org.openmrs.projectbuendia.webservices.rest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.BaseOpenmrsMetadata;
import org.openmrs.Field;
import org.openmrs.Form;
import org.openmrs.FormField;
import org.openmrs.Provider;
import org.openmrs.api.FormService;
import org.openmrs.api.ProviderService;
import org.openmrs.api.context.Context;
import org.openmrs.module.webservices.rest.SimpleObject;
import org.openmrs.module.webservices.rest.web.RequestContext;
import org.openmrs.module.webservices.rest.web.annotation.Resource;
import org.openmrs.module.webservices.rest.web.representation.Representation;
import org.openmrs.module.xforms.buendia.BuendiaXformBuilderEx;
import org.openmrs.module.xforms.buendia.FormData;
import org.openmrs.module.xforms.util.XformsUtil;
import org.openmrs.util.FormConstants;
import org.projectbuendia.openmrs.webservices.rest.RestController;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.annotation.Nullable;

import static org.openmrs.projectbuendia.webservices.rest.XmlUtil.getElementOrThrowNS;
import static org.openmrs.projectbuendia.webservices.rest.XmlUtil.removeNode;
import static org.openmrs.projectbuendia.webservices.rest.XmlUtil.toElementIterable;

/**
 * Resource for "form models" (not-yet-filled-in forms).   Note: this is under
 * org.openmrs as otherwise the resource annotation isn't picked up.
 * @see AbstractReadOnlyResource
 */
@Resource(name = RestController.REST_VERSION_1_AND_NAMESPACE
        + "/xforms", supportedClass = Form.class, supportedOpenmrsVersions = "1.10.*,1.11.*")
public class XformResource extends AbstractReadOnlyResource<Form> {
    private static final String HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
    private static final String XFORMS_NAMESPACE = "http://www.w3.org/2002/xforms";

    @SuppressWarnings("unused")
    private final Log log = LogFactory.getLog(getClass());

    private final FormService formService;
    private final ProviderService providerService;

    public XformResource() {
        super("xform", Representation.DEFAULT, Representation.FULL, Representation.REF);
        this.formService = Context.getFormService();
        this.providerService = Context.getProviderService();
    }

    /**
     * Adds the following fields to the {@link SimpleObject}:
     * <ul>
     * <li>name: display name of the form
     * <li>date_created: the date the form was created, as ms since epoch
     * <li>version: the version number of the form (e.g. 0.2.1)
     * <li>date_changed: the date the form was last modified, as ms since epoch;
     * for forms that contain a provider field, this date will also be
     * updated whenever the set of providers on the server changes
     * </ul>
     * <p/>
     * If the query parameter "?v=full" is present, also adds the "xml" field
     * containing the XML of the form model definition.
     * @param context      the request context; specify "v=full" in the URL params
     *                     to include the XML for the form model in the response
     * @param snapshotTime ignored
     */
    @Override
    protected void populateJsonProperties(Form form, RequestContext context, SimpleObject json, long snapshotTime) {
        json.add("name", form.getName());
        json.add("id", form.getFormId());
        json.add("version", form.getVersion());
        Date dateChanged = form.getDateChanged();
        json.add("date_created", form.getDateCreated());
        json.add("version", form.getVersion());
        boolean includesProviders = false;
        if (context.getRepresentation() == Representation.FULL) {
            try {
                // TODO: Use description instead of name?
                FormData formData = BuendiaXformBuilderEx.buildXform(form, new BuendiaXformCustomizer());
                String xml = convertToOdkCollect(formData.xml, form.getName());
                includesProviders = formData.includesProviders;
                xml = removeRelationshipNodes(xml);
                json.add("xml", xml);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        } else {
            // Do a linear search, as otherwise it puts too many assumptions on
            // comparison order.  Also FormField overrides compare to be based
            // on lots of fields, but leaves .equals() based on UUID unchanged,
            // which is really dangerous.
            for (FormField formField : form.getFormFields()) {
                Field field = formField.getField();
                if (FormConstants.FIELD_TYPE_DATABASE.equals(field.getFieldType().getFieldTypeId())
                        && "encounter".equals(field.getTableName())) {
                    includesProviders = true;
                }
                dateChanged = maxDate(dateChanged, dateChanged(formField));
            }
        }
        if (includesProviders) {
            for (Provider provider : providerService.getAllProviders()) {
                dateChanged = maxDate(dateChanged, dateChanged(provider));
            }
        }
        json.add("date_changed", dateChanged);
    }

    /**
     * Converts a vanilla Xform into one that ODK Collect is happy to work with.
     * This requires:
     * <ul>
     * <li>Changing the namespace of the the root element to http://www.w3.org/1999/xhtml
     * <li>Wrapping the model element in an HTML head element, with a title child element
     * <li>Wrapping remaining elements in an HTML body element
     * </ul>
     */
    static String convertToOdkCollect(String xml, String title) throws IOException, SAXException {
        // Change the namespace of the root element. I haven't figured out a way
        // to do
        // this within a document; removing the root element from the document
        // seems
        // to do odd things... so instead, we import it into a new document.
        Document oldDoc = XmlUtil.parse(xml);
        Document doc = XmlUtil.getDocumentBuilder().newDocument();
        Element root = (Element) doc.importNode(oldDoc.getDocumentElement(), true);
        root = (Element) doc.renameNode(root, HTML_NAMESPACE, "h:form");
        doc.appendChild(root);

        // Prepare the new wrapper elements
        Element head = doc.createElementNS(HTML_NAMESPACE, "h:head");
        Element titleElement = XmlUtil.appendElementNS(head, HTML_NAMESPACE, "h:title");
        titleElement.setTextContent(title);
        Element body = doc.createElementNS(HTML_NAMESPACE, "h:body");

        // Find the model element to go in the head, and all its following
        // siblings to go in the body.
        // We do this before moving any elements, for the sake of sanity.
        Element model = getElementOrThrowNS(root, XFORMS_NAMESPACE, "model");
        List<Node> nodesAfterModel = new ArrayList<>();
        Node nextSibling = model.getNextSibling();
        while (nextSibling != null) {
            nodesAfterModel.add(nextSibling);
            nextSibling = nextSibling.getNextSibling();
        }

        // Now we're done with the preparation, we can move everything.
        head.appendChild(model);
        for (Node node : nodesAfterModel) {
            body.appendChild(node);
        }

        // Having removed the model and everything after it, we can now just
        // append the head and body to the document element...
        root.appendChild(head);
        root.appendChild(body);

        return XformsUtil.doc2String(doc);
    }

    /**
     * Removes the relationship nodes added (unconditionally) by xforms. If
     * XFRM-189 is fixed, this method can go away.
     */
    static String removeRelationshipNodes(String xml) throws IOException, SAXException {
        Document doc = XmlUtil.parse(xml);
        removeBinding(doc, "patient_relative");
        removeBinding(doc, "patient_relative.person");
        removeBinding(doc, "patient_relative.relationship");

        for (Element relative : toElementIterable(doc.getElementsByTagNameNS("", "patient_relative"))) {
            removeNode(relative);
        }

        // Remove every parent of a label element with a text of
        // "RELATIONSHIPS". (Easiest way to find the ones added...)
        for (Element label : toElementIterable(doc.getElementsByTagNameNS(XFORMS_NAMESPACE, "label"))) {
            Element parent = (Element) label.getParentNode();
            if (XFORMS_NAMESPACE.equals(parent.getNamespaceURI()) && parent.getLocalName().equals("group")
                    && "RELATIONSHIPS".equals(label.getTextContent())) {
                removeNode(parent);
                // We don't need to find other labels now, especially if they
                // may already have been removed.
                break;
            }
        }
        return XformsUtil.doc2String(doc);
    }

    /** Returns the later of two nullable dates. */
    private Date maxDate(@Nullable Date d1, @Nullable Date d2) {
        if (d1 == null)
            return d2;
        if (d2 == null)
            return d1;
        return d1.before(d2) ? d2 : d1;
    }

    /**
     * Returns the actual last modification time of an OpenMRS object.
     * Because OpenMRS doesn't set the modification time upon initial
     * creation (sigh) we have to check both dateChanged and dateCreated.
     */
    private Date dateChanged(BaseOpenmrsMetadata d) {
        Date dateChanged = d.getDateChanged();
        if (dateChanged != null)
            return dateChanged;
        return d.getDateCreated();
    }

    // Visible for testing

    private static void removeBinding(Document doc, String id) {
        for (Element binding : toElementIterable(doc.getElementsByTagNameNS(XFORMS_NAMESPACE, "bind"))) {
            if (binding.getAttribute("id").equals(id)) {
                removeNode(binding);
                return;
            }
        }
    }

    // VisibleForTesting

    /**
     * Retrieves a single xform with the given UUID.  See
     * {@link #populateJsonProperties(Form, RequestContext, SimpleObject, long)}
     * for details on the context and snapshotTime arguments.
     * @param context      unused here
     * @param snapshotTime unused here
     * @see AbstractReadOnlyResource#retrieve(String, RequestContext)
     */
    @Override
    protected Form retrieveImpl(String uuid, RequestContext context, long snapshotTime) {
        return formService.getFormByUuid(uuid);
    }

    /**
     * Returns all xforms (there is no support for query parameters).  See
     * {@link #populateJsonProperties(Form, RequestContext, SimpleObject, long)}
     * for details on the context and snapshotTime arguments.
     * @param context      unused here
     * @param snapshotTime unused here
     *                     Note: because of a bug in parsing form definitions, "v=full" is currently
     *                     broken for this function
     * @see AbstractReadOnlyResource#search(RequestContext)
     */
    @Override
    protected Iterable<Form> searchImpl(RequestContext context, long snapshotTime) {
        // TODO/bug: Fix verbose mode. Currently produces the error:
        // "No bind node for bindName _3._bleeding_sites".
        // No query parameters supported - just give all the forms
        List<Form> forms = new ArrayList<>();
        for (Form form : formService.getAllForms()) {
            if (form.getPublished()) {
                forms.add(form);
            }
        }
        return forms;
    }
}