csiro.pidsvc.mappingstore.Manager.java Source code

Java tutorial

Introduction

Here is the source code for csiro.pidsvc.mappingstore.Manager.java

Source

/*
 * CSIRO Open Source Software License Agreement (variation of the BSD / MIT License)
 * 
 * Copyright (c) 2013, Commonwealth Scientific and Industrial Research Organisation (CSIRO)
 * ABN 41 687 119 230.
 * 
 * All rights reserved. This code is licensed under CSIRO Open Source Software
 * License Agreement license, available at the root application directory.
 */

package csiro.pidsvc.mappingstore;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
import javax.xml.XMLConstants;
import javax.xml.bind.ValidationException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import net.sf.saxon.s9api.Processor;
import net.sf.saxon.s9api.QName;
import net.sf.saxon.s9api.SaxonApiException;
import net.sf.saxon.s9api.Serializer;
import net.sf.saxon.s9api.XPathCompiler;
import net.sf.saxon.s9api.XdmAtomicValue;
import net.sf.saxon.s9api.XdmItem;
import net.sf.saxon.s9api.XdmNode;
import net.sf.saxon.s9api.XdmSequenceIterator;
import net.sf.saxon.s9api.XdmValue;
import net.sf.saxon.s9api.XsltCompiler;
import net.sf.saxon.s9api.XsltExecutable;
import net.sf.saxon.s9api.XsltTransformer;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.StringUtils;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.simple.JSONObject;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.SAXException;

import csiro.pidsvc.core.Settings;
import csiro.pidsvc.helper.Http;
import csiro.pidsvc.helper.JSONObjectHelper;
import csiro.pidsvc.helper.Stream;
import csiro.pidsvc.helper.URI;
import csiro.pidsvc.mappingstore.action.List;
import csiro.pidsvc.mappingstore.condition.AbstractCondition;
import csiro.pidsvc.mappingstore.condition.ConditionContentType;
import csiro.pidsvc.mappingstore.condition.ConditionQrCodeRequest;
import csiro.pidsvc.mappingstore.condition.SpecialConditionType;

/**
 * Manager class encapsulates application/database interaction logic.
 * 
 * @author Pavel Golodoniuc, CSIRO Mineral Resources Flagship
 */
public class Manager {
    private static Logger _logger = LogManager.getLogger(Manager.class.getName());

    protected Connection _connection = null;
    private String _authorizationName = null;

    public class MappingMatchResults {
        public static final int NULL = -1;

        public final int MappingId;
        public final int DefaultActionId;
        public final AbstractCondition Condition;
        public final Object AuxiliaryData;

        public MappingMatchResults(int mappingId, int defaultActionId, AbstractCondition condition,
                Object auxiliaryData) {
            MappingId = mappingId;
            DefaultActionId = defaultActionId;
            Condition = condition;
            AuxiliaryData = auxiliaryData;
        }

        public boolean success() {
            return MappingId != MappingMatchResults.NULL || DefaultActionId != MappingMatchResults.NULL
                    || Condition != null || AuxiliaryData != null;
        }
    }

    public class MappingParentDescriptor {
        public final int MappingId;
        public final String MappingPath;
        public final int DefaultActionId;
        public final Object Aux;

        public MappingParentDescriptor(int mappingId, String mappingPath, int defaultActionId, Object aux) {
            MappingId = mappingId;
            MappingPath = mappingPath;
            DefaultActionId = defaultActionId;
            Aux = aux;
        }
    }

    public class MappingParentDescriptorList extends Vector<MappingParentDescriptor> {
        private static final long serialVersionUID = 8137846998774342633L;

        public MappingParentDescriptorList(int initialCapacity) {
            super(initialCapacity);
        }

        public boolean contains(int mappingId) {
            for (MappingParentDescriptor it : this) {
                if (it.MappingId == mappingId)
                    return true;
            }
            return false;
        }

        public MappingParentDescriptor getFirstDefaultActionMapping() {
            for (MappingParentDescriptor it : this) {
                if (it.DefaultActionId != MappingMatchResults.NULL)
                    return it;
            }
            return null;
        }
    }

    protected interface ICallback {
        public String process(InputStream inputStream) throws Exception;
    }

    /**************************************************************************
     *  Case-sensitivity 'structure' class and properties.
     */

    protected class CaseSensitivity {
        public boolean IsCaseSensitive = true;
        public int RegularExpressionFlags = 0;
    }

    protected CaseSensitivity _caseSensitivity = new CaseSensitivity();

    protected CaseSensitivity getCaseSensitivity() {
        return _caseSensitivity;
    }

    protected void refreshCaseSensitivity() {
        String caseSensitive = getSetting("CaseSensitiveURI");
        _caseSensitivity.IsCaseSensitive = caseSensitive != null && caseSensitive.equalsIgnoreCase("1");
        _caseSensitivity.RegularExpressionFlags = _caseSensitivity.IsCaseSensitive ? 0 : Pattern.CASE_INSENSITIVE;
    }

    /**************************************************************************
     *  Construction/destruction.
     */

    public Manager() throws NamingException, SQLException, IOException {
        InitialContext initCtx = new InitialContext();
        Context envCtx = (Context) initCtx.lookup("java:comp/env");
        DataSource ds = (DataSource) envCtx.lookup(Settings.getInstance().getProperty("jndiReferenceName"));
        _connection = ds.getConnection();
        refreshCaseSensitivity();
    }

    public Manager(HttpServletRequest request) throws NamingException, SQLException, IOException {
        this();

        // Try to retrieve authentication details using Java API.
        _authorizationName = request.getRemoteUser();

        // If it fails try to read 'authorization' HTTP header directly.
        if (_authorizationName == null) {
            String authHeader = request.getHeader("authorization");
            if (authHeader != null && !authHeader.isEmpty() && authHeader.startsWith("Basic")) {
                // Extract user name from basic authentication HTTP header.
                authHeader = authHeader.substring(authHeader.indexOf(' '));
                authHeader = StringUtils.newStringUtf8(Base64.decodeBase64(authHeader));
                _authorizationName = authHeader.substring(0, authHeader.indexOf(':'));
            }
        }
    }

    public void close() {
        try {
            _connection.close();
        } catch (SQLException e) {
            _logger.error("Closing connection failed.", e);
        } finally {
            _connection = null;
        }
    }

    /**************************************************************************
     *  Authentication/authorisation methods.
     */

    public String getAuthorizationName() {
        return _authorizationName;
    }

    /**************************************************************************
     *  Generic processing methods.
     */

    protected void validateRequest(String inputData, String xmlSchemaResourcePath)
            throws IOException, ValidationException {
        try {
            SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
            schemaFactory.setResourceResolver(new LSResourceResolver() {
                @Override
                public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId,
                        String baseURI) {
                    return new XsdSchemaResolver(type, namespaceURI, publicId, systemId, baseURI);
                }
            });

            Schema schema = schemaFactory
                    .newSchema(new StreamSource(getClass().getResourceAsStream(xmlSchemaResourcePath)));
            Validator validator = schema.newValidator();
            _logger.trace("Validating XML Schema.");
            validator.validate(new StreamSource(new StringReader(inputData)));
        } catch (SAXException ex) {
            _logger.debug("Unknown format.", ex);
            throw new ValidationException("Unknown format.", ex);
        }
    }

    protected String processGenericXmlCommand(InputStream inputStream, String xmlSchemaResourcePath,
            String xsltResourcePath) throws SaxonApiException, SQLException, IOException, ValidationException {
        return processGenericXmlCommand(Stream.readInputStream(inputStream), xmlSchemaResourcePath,
                xsltResourcePath);
    }

    protected String processGenericXmlCommand(String inputData, String xmlSchemaResourcePath,
            String xsltResourcePath) throws SaxonApiException, SQLException, IOException, ValidationException {
        // Validate request.
        if (xmlSchemaResourcePath != null)
            validateRequest(inputData, xmlSchemaResourcePath);

        // Generate SQL query.
        Processor processor = new Processor(false);
        XsltCompiler xsltCompiler = processor.newXsltCompiler();
        InputStream inputSqlGen = getClass().getResourceAsStream(xsltResourcePath);
        XsltExecutable xsltExec = xsltCompiler.compile(new StreamSource(inputSqlGen));
        XsltTransformer transformer = xsltExec.load();

        StringWriter swSqlQuery = new StringWriter();
        transformer.setInitialContextNode(
                processor.newDocumentBuilder().build(new StreamSource(new StringReader(inputData))));
        transformer.setDestination(new Serializer(swSqlQuery));
        transformer.setParameter(new QName("AuthorizationName"), new XdmAtomicValue(getAuthorizationName()));
        _logger.trace("Generating SQL query.");
        transformer.transform();

        // Update mappings in the database.
        Statement st = null;
        try {
            String sqlQuery = swSqlQuery.toString();
            st = _connection.createStatement();
            st.execute(sqlQuery);

            Pattern re = Pattern.compile("^--((?:OK|ERROR): .*)", Pattern.CASE_INSENSITIVE);
            Matcher m = re.matcher(sqlQuery);

            if (m.find())
                return m.group(1);
        } finally {
            if (st != null)
                st.close();
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    protected String unwrapCompressedBackupFile(HttpServletRequest request, ICallback callback) {
        java.util.List<FileItem> fileList = null;
        GZIPInputStream gis = null;
        String ret = null;

        try {
            DiskFileItemFactory fileItemFactory = new DiskFileItemFactory();

            // Set the size threshold, above which content will be stored on disk.
            fileItemFactory.setSizeThreshold(1 * 1024 * 1024); // 1 MB
            //         fileItemFactory.setSizeThreshold(100 * 1024); // 100 KB

            // Set the temporary directory to store the uploaded files of size above threshold.
            fileItemFactory.setRepository(new File(System.getProperty("java.io.tmpdir")));

            ServletFileUpload uploadHandler = new ServletFileUpload(fileItemFactory);

            fileList = uploadHandler.parseRequest(request);
            for (FileItem item : fileList) {
                if (item.isFormField())
                    continue;

                try {
                    // Try to restore the backup file as it was in binary format.
                    gis = new GZIPInputStream(item.getInputStream());
                    ret = callback.process(gis);
                    gis.close();
                } catch (IOException ex) {
                    String msg = ex.getMessage();
                    if (msg != null && msg.equalsIgnoreCase("Not in GZIP format")) {
                        // Try to restore the backup file as it was unzipped.
                        ret = callback.process(item.getInputStream());
                    } else
                        throw ex;
                }

                // Process the first uploaded file only.
                return ret;
            }
        } catch (Exception ex) {
            String msg = ex.getMessage();
            Throwable linkedException = ex.getCause();
            _logger.warn(msg);
            if (linkedException != null)
                _logger.warn(linkedException.getMessage());
            if (msg != null && msg.equalsIgnoreCase("Not in GZIP format"))
                return "ERROR: Unknown file format.";
            else
                return "ERROR: " + (msg == null ? "Something went wrong."
                        : msg + (linkedException == null ? "" : " " + linkedException.getMessage()));
        } finally {
            try {
                // Close the stream.
                gis.close();
            } catch (Exception ex) {
            }
            if (fileList != null) {
                // Delete all uploaded files.
                for (FileItem item : fileList) {
                    if (!item.isFormField() && !item.isInMemory())
                        ((DiskFileItem) item).delete();
                }
            }
        }
        _logger.trace("No file found.");
        return "ERROR: No file.";
    }

    /**************************************************************************
     *  URI Mappings.
     */

    public String createMapping(InputStream inputStream, boolean isBackup)
            throws SaxonApiException, SQLException, IOException, ValidationException {
        return createMapping(Stream.readInputStream(inputStream), isBackup);
    }

    public String createMapping(String inputData, boolean isBackup)
            throws SaxonApiException, SQLException, IOException, ValidationException {
        String ret = processGenericXmlCommand(inputData, "xsd/" + (isBackup ? "backup" : "mapping") + ".xsd",
                "xslt/import_mapping_postgresql.xslt");
        if (isBackup) {
            boolean noMappings = ret.equalsIgnoreCase("OK: No mapping rules found in the backup.");
            String otherRestoreStatus = "";

            // Restore condition sets.
            String retSubset = createConditionSet(inputData);
            if (retSubset.startsWith("OK: Success"))
                otherRestoreStatus += "\n" + retSubset;

            // Restore lookup maps.
            retSubset = createLookup(inputData);
            if (retSubset.startsWith("OK: Success"))
                otherRestoreStatus += "\n" + retSubset;

            if (noMappings)
                ret = otherRestoreStatus.substring(1);
            else
                ret += otherRestoreStatus;
        }
        return ret;
    }

    public JSONObject mergeMappingByPath(String mappingPath, String inputData, boolean replace) {
        try {
            String targetMapping = exportMapping(mappingPath, false).replaceAll("<\\?xml.*?\\?>", "");
            return mergeMappingImpl(targetMapping, inputData, replace);
        } catch (Exception e) {
            return JSONObjectHelper.create("error", "ERROR: " + e.getMessage());
        }
    }

    public JSONObject mergeMappingImpl(String targetMapping, String inputData, boolean replace) {
        try {
            // Validate request.
            validateRequest(targetMapping, "xsd/backup.xsd");
            validateRequest(inputData, "xsd/backup.xsd");

            // Generate merged mapping.
            String xsltInput = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><merge><target>" + targetMapping
                    + "</target><source>" + inputData.replaceAll("<\\?xml.*?\\?>", "") + "</source></merge>";
            Processor processor = new Processor(false);
            XsltCompiler xsltCompiler = processor.newXsltCompiler();
            InputStream inputSqlGen = getClass().getResourceAsStream("xslt/merge.xslt");
            XsltExecutable xsltExec = xsltCompiler.compile(new StreamSource(inputSqlGen));
            XsltTransformer transformer = xsltExec.load();

            StringWriter swMergedMapping = new StringWriter();
            transformer.setInitialContextNode(
                    processor.newDocumentBuilder().build(new StreamSource(new StringReader(xsltInput))));
            transformer.setDestination(new Serializer(swMergedMapping));
            transformer.setParameter(new QName("", "replace"), new XdmAtomicValue(replace ? 1 : 0));
            _logger.trace("Generating merged mapping.");
            transformer.transform();

            // Process merged mapping.
            String mergedMapping = swMergedMapping.toString();
            XPathCompiler xpathCompiler = processor.newXPathCompiler();
            XdmNode mergedXml = processor.newDocumentBuilder()
                    .build(new StreamSource(new StringReader(mergedMapping)));
            XdmItem node = xpathCompiler.evaluateSingle("/response/merged/*[1]", mergedXml);

            if (node != null) {
                String retCreateMapping = createMapping(node.toString(), false);
                _logger.trace(retCreateMapping);
                if (retCreateMapping.startsWith("OK:")) {
                    XdmValue log = xpathCompiler.evaluate("/response/log/condition", mergedXml);
                    String conditionFlags = "";

                    // Get the list of conditions that have changed.
                    for (XdmSequenceIterator iter = log.iterator(); iter
                            .hasNext(); conditionFlags += iter.next().getStringValue())
                        ;

                    // Return JSON object.
                    return JSONObjectHelper.create("status", retCreateMapping, "conditionChangeFlags",
                            conditionFlags);
                } else {
                    // Error has occurred.
                    return JSONObjectHelper.create("error", retCreateMapping);
                }
            }

            node = xpathCompiler.evaluateSingle("/error", mergedXml);
            if (node != null)
                return JSONObjectHelper.create("error", node.getStringValue());
            return JSONObjectHelper.create("error",
                    "ERROR: Unexpected exception has occurred. No data has been changed.");
        } catch (Exception e) {
            return JSONObjectHelper.create("error", "ERROR: " + e.getMessage());
        }
    }

    public boolean deleteMapping(String mappingPath) throws SQLException {
        PreparedStatement pst = null;
        try {
            pst = _connection.prepareStatement(
                    "UPDATE mapping SET date_end = now() WHERE mapping_path = ? AND date_end IS NULL;");
            pst.setString(1, mappingPath);
            return pst.execute();
        } finally {
            if (pst != null)
                pst.close();
        }
    }

    public MappingParentDescriptor getCatchAllDescriptor() throws SQLException {
        PreparedStatement pst = null;
        ResultSet rs = null;
        int defaultActionId = MappingMatchResults.NULL;

        try {
            pst = _connection.prepareStatement(
                    "SELECT mapping_id, default_action_id FROM vw_active_mapping WHERE mapping_path IS NULL AND type = 'Regex'");
            if (pst.execute()) {
                rs = pst.getResultSet();
                if (rs.next()) {
                    defaultActionId = rs.getInt(2);
                    if (rs.wasNull())
                        defaultActionId = MappingMatchResults.NULL;
                    return new MappingParentDescriptor(rs.getInt(1), null, defaultActionId, true);
                }
            }
        } finally {
            if (rs != null)
                rs.close();
            if (pst != null)
                pst.close();
        }
        return null;
    }

    public MappingParentDescriptorList getParents(int mappingId, URI uri) throws SQLException {
        MappingParentDescriptorList ret = new MappingParentDescriptorList(1);
        PreparedStatement pst = null;
        ResultSet rs = null;
        int parentId;
        int defaultActionId = MappingMatchResults.NULL;
        String mappingPath, parentPath = null, mappingType = null;
        Object aux = null;

        try {
            pst = _connection.prepareStatement(
                    "SELECT mapping_path, parent, type, default_action_id FROM vw_active_mapping WHERE mapping_id = ? AND mapping_path IS NOT NULL");
            pst.setInt(1, mappingId);

            // Get initial mapping.
            if (pst.execute()) {
                rs = pst.getResultSet();
                if (rs.next()) {
                    mappingPath = rs.getString(1);
                    parentPath = rs.getString(2);
                    mappingType = rs.getString(3);

                    defaultActionId = rs.getInt(4);
                    if (rs.wasNull())
                        defaultActionId = MappingMatchResults.NULL;

                    aux = mappingType.equalsIgnoreCase("1:1") ? true
                            : Pattern.compile(mappingPath, getCaseSensitivity().RegularExpressionFlags);
                    ret.add(new MappingParentDescriptor(mappingId, mappingPath, defaultActionId, aux));
                }
            }
            rs.close();
            pst.close();
            rs = null;
            pst = null;

            // Get parents.
            while (parentPath != null) {
                pst = _connection.prepareStatement(
                        "SELECT mapping_id, mapping_path, parent, default_action_id FROM vw_active_mapping WHERE mapping_path = ? AND type = 'Regex'");
                pst.setString(1, parentPath);

                parentPath = null;
                if (pst.execute()) {
                    rs = pst.getResultSet();
                    if (rs.next()) {
                        parentId = rs.getInt(1);
                        mappingPath = rs.getString(2);
                        parentPath = rs.getString(3);

                        defaultActionId = rs.getInt(4);
                        if (rs.wasNull())
                            defaultActionId = MappingMatchResults.NULL;

                        // Prevent cyclic inheritance syndrome.
                        if (ret.contains(parentId))
                            break;

                        // Check that parent pattern matches URI.
                        Pattern re = Pattern.compile(mappingPath, getCaseSensitivity().RegularExpressionFlags);
                        Matcher m = re.matcher(uri.getPathNoExtension());
                        if (!m.find())
                            break;

                        // Add new parent to the list.
                        ret.add(new MappingParentDescriptor(parentId, rs.getString(2), defaultActionId, re));
                    }
                }
            }

            // Get catch-all mapping descriptor.
            MappingParentDescriptor catchAll = getCatchAllDescriptor();
            if (catchAll != null)
                ret.add(catchAll);
        } finally {
            if (rs != null)
                rs.close();
            if (pst != null)
                pst.close();
        }
        return ret;
    }

    public boolean checkNonCyclicInheritance(int mappingId) throws SQLException {
        Vector<Integer> ret = new Vector<Integer>(1);
        PreparedStatement pst = null;
        ResultSet rs = null;
        int parentId;
        String parentPath = null;

        try {
            pst = _connection.prepareStatement(
                    "SELECT parent FROM vw_active_mapping WHERE mapping_id = ? AND mapping_path IS NOT NULL");
            pst.setInt(1, mappingId);

            // Get initial mapping.
            if (pst.execute()) {
                rs = pst.getResultSet();
                if (rs.next()) {
                    parentPath = rs.getString(1);
                    ret.add(mappingId);
                }
            }
            rs.close();
            pst.close();
            rs = null;
            pst = null;

            // Get parents.
            while (parentPath != null) {
                pst = _connection.prepareStatement(
                        "SELECT mapping_id, parent FROM vw_active_mapping WHERE mapping_path = ? AND type = 'Regex'");
                pst.setString(1, parentPath);

                parentPath = null;
                if (pst.execute()) {
                    rs = pst.getResultSet();
                    if (rs.next()) {
                        parentId = rs.getInt(1);
                        parentPath = rs.getString(2);

                        // Cyclic inheritance encountered.
                        if (ret.contains(parentId))
                            return false;

                        // Add new parent to the list.
                        ret.add(parentId);
                    }
                }
            }
        } finally {
            if (rs != null)
                rs.close();
            if (pst != null)
                pst.close();
        }
        return true;
    }

    public MappingMatchResults findExactMatch(URI uri, HttpServletRequest request)
            throws SQLException, InstantiationException, IllegalAccessException, ClassNotFoundException,
            SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
        PreparedStatement pst = null;
        try {
            if (isCaseSensitive()) {
                pst = _connection.prepareStatement(
                        "SELECT mapping_id, mapping_path, type FROM vw_active_mapping WHERE mapping_path = ? AND type = '1:1'");
                pst.setString(1, uri.getPathNoExtension());
            } else {
                pst = _connection.prepareStatement(
                        "SELECT mapping_id, mapping_path, type FROM vw_active_mapping WHERE LOWER(mapping_path) = ? AND type = '1:1'");
                pst.setString(1, uri.getPathNoExtension().toLowerCase());
            }
            return findMatchImpl(pst, uri, request, false);
        } finally {
            if (pst != null)
                pst.close();
        }
    }

    public MappingMatchResults findRegexMatch(URI uri, HttpServletRequest request)
            throws SQLException, InstantiationException, IllegalAccessException, ClassNotFoundException,
            IllegalArgumentException, SecurityException, InvocationTargetException, NoSuchMethodException {
        PreparedStatement pst = null;
        try {
            /* Recursive SQL query ensures the pattern based matching starts from
             * the deepest level up to Catch-All mapping.
             */
            pst = _connection.prepareStatement("WITH RECURSIVE F (mapping_id, mapping_path, type, level)\n" + "AS\n"
                    + "(\n" + "   SELECT a.mapping_id, a.mapping_path, a.type, 0\n"
                    + "   FROM vw_active_mapping a\n"
                    + "   WHERE a.type = 'Regex' AND a.mapping_path IS NOT NULL AND a.parent IS NULL\n"
                    + "   UNION ALL\n" + "   SELECT a.mapping_id, a.mapping_path, a.type, level + 1\n"
                    + "   FROM vw_active_mapping a\n" + "      INNER JOIN F ON F.mapping_path = a.parent\n"
                    + "   WHERE a.type = 'Regex' AND a.mapping_path IS NOT NULL\n" + ")\n"
                    + "SELECT mapping_id, mapping_path, type FROM F ORDER BY level DESC");
            return findMatchImpl(pst, uri, request, true);
        } finally {
            if (pst != null)
                pst.close();
        }
    }

    private MappingMatchResults findMatchImpl(PreparedStatement pst, URI uri, HttpServletRequest request,
            boolean patternBased)
            throws SQLException, InstantiationException, IllegalAccessException, ClassNotFoundException,
            IllegalArgumentException, SecurityException, InvocationTargetException, NoSuchMethodException {
        ResultSet rs = null;
        int mappingId = MappingMatchResults.NULL;
        int defaultActionId = MappingMatchResults.NULL;
        AbstractCondition retCondition = null;
        Object matchAuxiliaryData = null;
        Pattern re;
        Matcher m;

        MappingParentDescriptorList parents = null;
        boolean isOneToOne = false;

        try {
            if (pst.execute()) {
                rs = pst.getResultSet();
                while (rs.next()) {
                    // Check pattern match for pattern based mappings.
                    if (!(isOneToOne = rs.getString("type").equalsIgnoreCase("1:1"))) {
                        re = Pattern.compile(rs.getString("mapping_path"),
                                getCaseSensitivity().RegularExpressionFlags);
                        m = re.matcher(uri.getPathNoExtension());
                        if (!m.find())
                            continue;
                    }

                    // Get mapping hierarchy.
                    parents = getParents(rs.getInt("mapping_id"), uri);

                    // Break the loop once first matching pattern is found.
                    break;
                }

                // If there're no matching pattern based mappings were found then resort to Catch-all.
                if (patternBased && parents == null) {
                    MappingParentDescriptor catchAll = getCatchAllDescriptor();
                    parents = new MappingParentDescriptorList(1);
                    if (catchAll != null)
                        parents.add(catchAll);
                }

                // Iterate through the inheritance tree up to Catch-All mapping to find a matching condition.
                if (parents != null) {
                    for (MappingParentDescriptor parent : parents) {
                        // Find matching condition.
                        retCondition = getCondition(parent.MappingId, uri, request, parent.Aux);
                        if (retCondition != null) {
                            mappingId = parent.MappingId;
                            matchAuxiliaryData = parent.Aux;
                            break;
                        }
                    }

                    // Iterate through the inheritance tree up to Catch-All mapping to find a default action.
                    if (retCondition == null) {
                        MappingParentDescriptor defaultActionMapping = parents.getFirstDefaultActionMapping();
                        if (defaultActionMapping != null) {
                            mappingId = defaultActionMapping.MappingId;
                            defaultActionId = defaultActionMapping.DefaultActionId;
                            matchAuxiliaryData = defaultActionMapping.Aux;
                        } else if (isOneToOne) {
                            // Set a flag that one-to-one mapping has been found but neither matching conditions nor
                            // default actions defined.
                            matchAuxiliaryData = true;
                        }
                    }
                }
            } //- pst.execute()
        } finally {
            if (rs != null)
                rs.close();
        }
        return new MappingMatchResults(mappingId, defaultActionId, retCondition, matchAuxiliaryData);
    }

    private Vector<csiro.pidsvc.mappingstore.condition.Descriptor> getConditionsByMappingId(int mappingId)
            throws SQLException {
        return getConditionsImpl("mapping_id", mappingId);
    }

    private Vector<csiro.pidsvc.mappingstore.condition.Descriptor> getConditionsBySetId(int conditionSetId)
            throws SQLException {
        return getConditionsImpl("condition_set_id", conditionSetId);
    }

    private Vector<csiro.pidsvc.mappingstore.condition.Descriptor> getConditionsBySetName(String conditionSetName)
            throws SQLException {
        return getConditionsImpl("condition_set_id", getConditionSetId(conditionSetName));
    }

    private Vector<csiro.pidsvc.mappingstore.condition.Descriptor> getConditionsImpl(String searchField, int id)
            throws SQLException {
        PreparedStatement pst = null;
        ResultSet rs = null;

        try {
            pst = _connection.prepareStatement(
                    "SELECT * FROM condition WHERE " + searchField + " = ? ORDER BY condition_id");
            pst.setInt(1, id);

            if (!pst.execute())
                return null;

            // Get list of conditions.
            Vector<csiro.pidsvc.mappingstore.condition.Descriptor> conditions = new Vector<csiro.pidsvc.mappingstore.condition.Descriptor>();
            for (rs = pst.getResultSet(); rs.next(); conditions
                    .add(new csiro.pidsvc.mappingstore.condition.Descriptor(rs.getInt("condition_id"),
                            rs.getString("type"), rs.getString("match"), rs.getString("description"))))
                ;
            return conditions;
        } catch (Exception e) {
            _logger.error(e);
        } finally {
            if (rs != null)
                rs.close();
            if (pst != null)
                pst.close();
        }
        return null;
    }

    protected AbstractCondition getCondition(int mappingId, URI uri, HttpServletRequest request,
            Object matchAuxiliaryData) throws SQLException {
        // QR Code request.
        if (uri.isQrCodeRequest())
            return new ConditionQrCodeRequest(uri, request);

        // Get matching condition.
        return getMatchingCondition(getConditionsByMappingId(mappingId), uri, request, matchAuxiliaryData);
    }

    protected AbstractCondition getMatchingCondition(
            Vector<csiro.pidsvc.mappingstore.condition.Descriptor> conditions, URI uri, HttpServletRequest request,
            Object matchAuxiliaryData) throws SQLException {
        if (conditions == null)
            return null;

        try {
            Vector<ConditionContentType> prioritizedConditions = null;
            for (csiro.pidsvc.mappingstore.condition.Descriptor descriptor : conditions) {
                /*
                 * Once ContentType condition is encountered process all
                 * ContentType conditions in one go as a group.
                 * 
                 * Skip if ContentType conditions have already been processed.
                 */
                if (prioritizedConditions == null && descriptor.Type.equalsIgnoreCase("ContentType")) {
                    // Extract all ContentType conditions.
                    prioritizedConditions = new Vector<ConditionContentType>();
                    for (csiro.pidsvc.mappingstore.condition.Descriptor dctr : conditions) {
                        if (dctr.Type.equalsIgnoreCase("ContentType")) {
                            Class<?> impl = Class
                                    .forName("csiro.pidsvc.mappingstore.condition.Condition" + dctr.Type);
                            Constructor<?> ctor = impl.getDeclaredConstructor(URI.class, HttpServletRequest.class,
                                    int.class, String.class, Object.class);
                            prioritizedConditions.add((ConditionContentType) ctor.newInstance(uri, request, dctr.ID,
                                    dctr.Match, matchAuxiliaryData));
                        }
                    }

                    // Find matching conditions.
                    AbstractCondition matchingCondition = ConditionContentType
                            .getMatchingCondition(prioritizedConditions);
                    if (matchingCondition != null)
                        return matchingCondition;

                    // Continue if no matching conditions were found.
                    continue;
                }

                /*
                 * Special handling for ConditionSet type.
                 */
                if (descriptor.Type.equalsIgnoreCase("ConditionSet")) {
                    // Block using a ConditionSet within another ConditionSet.
                    if (Thread.currentThread().getStackTrace()[2].getMethodName()
                            .equalsIgnoreCase("getMatchingCondition"))
                        continue;

                    // Get conditions from the set.
                    AbstractCondition matchingCondition = getMatchingCondition(
                            getConditionsBySetName(descriptor.Match), uri, request, matchAuxiliaryData);
                    if (matchingCondition != null) {
                        matchingCondition.Set = descriptor.Match;
                        return matchingCondition;
                    }

                    // Continue if no matching conditions in the subset were found.
                    continue;
                }

                // Process all other conditions.
                Class<?> impl = Class.forName("csiro.pidsvc.mappingstore.condition.Condition" + descriptor.Type);
                Constructor<?> ctor = impl.getDeclaredConstructor(URI.class, HttpServletRequest.class, int.class,
                        String.class, Object.class);

                AbstractCondition condition = (AbstractCondition) ctor.newInstance(uri, request, descriptor.ID,
                        descriptor.Match, matchAuxiliaryData);
                if (condition.matches())
                    return condition;
            }
        } catch (Exception e) {
            _logger.error(e);
        }
        return null;
    }

    public csiro.pidsvc.mappingstore.action.Descriptor getAction(int actionId) throws SQLException {
        PreparedStatement pst = null;
        ResultSet rs = null;

        try {
            pst = _connection.prepareStatement("SELECT * FROM action WHERE action_id = ?");
            pst.setInt(1, actionId);

            rs = pst.executeQuery();
            rs.next();
            return new csiro.pidsvc.mappingstore.action.Descriptor(rs.getString("type"),
                    rs.getString("action_name"), rs.getString("action_value"));
        } finally {
            if (rs != null)
                rs.close();
            if (pst != null)
                pst.close();
        }
    }

    public List getActionsByConditionId(int conditionId) throws SQLException {
        PreparedStatement pst = null;
        ResultSet rs = null;
        List actions = new List();

        switch (conditionId) {
        case SpecialConditionType.QR_CODE_REQUEST:
            actions.add(new csiro.pidsvc.mappingstore.action.Descriptor("QrCode", null, null));
            break;
        default: {
            try {
                pst = _connection.prepareStatement("SELECT * FROM action WHERE condition_id = ?");
                pst.setInt(1, conditionId);

                rs = pst.executeQuery();
                while (rs.next()) {
                    actions.add(new csiro.pidsvc.mappingstore.action.Descriptor(rs.getString("type"),
                            rs.getString("action_name"), rs.getString("action_value")));
                }
            } finally {
                if (rs != null)
                    rs.close();
                if (pst != null)
                    pst.close();
            }
        }
        }
        return actions;
    }

    /**************************************************************************
     *  Export/import.
     */

    public String backupDataStore(boolean fullBackup, boolean includeDeprecated, boolean includeConditionSets,
            boolean includeLookupMaps) throws SQLException {
        String source;
        if (includeDeprecated)
            source = fullBackup ? "mapping" : "vw_latest_mapping";
        else
            source = fullBackup ? "vw_full_mapping_activeonly" : "vw_active_mapping";
        return exportMappingsImpl(null, "db", fullBackup, source, true, includeConditionSets, includeLookupMaps);
    }

    public String exportMapping(int mappingId) throws SQLException {
        return exportMappingsImpl(mappingId, "record", false, "mapping", false, false, false);
    }

    public String exportMapping(String mappingPath, boolean fullBackup) throws SQLException {
        if (mappingPath == null || mappingPath.isEmpty())
            return exportCatchAllMapping(fullBackup);
        return exportMappingsImpl(mappingPath, "record", fullBackup, fullBackup ? "mapping" : "vw_latest_mapping",
                false, false, false);
    }

    public String exportCatchAllMapping(boolean fullBackup) throws SQLException {
        return exportMappingsImpl(0, "record", fullBackup, fullBackup ? "mapping" : "vw_latest_mapping", false,
                false, false);
    }

    protected String exportMappingsImpl(Object mappingIdentifier, String scope, boolean fullBackup, String source,
            boolean preserveDatesForDeprecatedMappings, boolean includeConditionSets, boolean includeLookupMaps)
            throws SQLException {
        PreparedStatement pst = null;
        ResultSet rs = null;
        String ret = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<backup type=\""
                + (fullBackup ? "full" : "partial") + "\" scope=\"" + scope
                + "\" xmlns=\"urn:csiro:xmlns:pidsvc:backup:1.0\">";
        int defaultActionId;
        Timestamp timeStamp;
        String buf, path;

        try {
            if (mappingIdentifier instanceof Integer) {
                if ((Integer) mappingIdentifier == 0) {
                    // Catch all mapping.
                    pst = _connection.prepareStatement(
                            "SELECT * FROM " + source + " WHERE mapping_path IS NULL ORDER BY mapping_id");
                } else {
                    pst = _connection.prepareStatement(
                            "SELECT * FROM " + source + " WHERE mapping_id = ? ORDER BY mapping_id");
                    pst.setInt(1, (Integer) mappingIdentifier);
                }
            } else {
                // Export mapping by path or all mappings.
                pst = _connection.prepareStatement("SELECT * FROM " + source
                        + (mappingIdentifier == null ? "" : " WHERE mapping_path = ?") + " ORDER BY mapping_id");
                if (mappingIdentifier != null)
                    pst.setString(1, (String) mappingIdentifier);
            }

            if (pst.execute()) {
                for (rs = pst.getResultSet(); rs.next();) {
                    path = rs.getString(
                            mappingIdentifier instanceof Integer || !fullBackup ? "original_path" : "mapping_path");

                    ret += "<mapping";

                    // Time stamps are only applicable for full backups and deprecated records.
                    if (fullBackup || !fullBackup && preserveDatesForDeprecatedMappings
                            && rs.getTimestamp("date_end") != null) {
                        timeStamp = rs.getTimestamp("date_start");
                        if (timeStamp != null)
                            ret += " date_start=\"" + timeStamp.toString().replace(" ", "T") + "Z\"";
                        timeStamp = rs.getTimestamp("date_end");
                        if (timeStamp != null)
                            ret += " date_end=\"" + timeStamp.toString().replace(" ", "T") + "Z\"";
                    }

                    // Preserve original mapping path for full backups.
                    if (fullBackup && path != null)
                        ret += " original_path=\"" + rs.getString("original_path") + "\"";

                    ret += ">"; // mapping

                    ret += (path == null ? "<path/>" : "<path>" + StringEscapeUtils.escapeXml(path) + "</path>");
                    buf = rs.getString("parent");
                    if (buf != null)
                        ret += "<parent>" + StringEscapeUtils.escapeXml(buf) + "</parent>";
                    ret += "<type>" + rs.getString("type") + "</type>";
                    buf = rs.getString("title");
                    if (buf != null)
                        ret += "<title>" + StringEscapeUtils.escapeXml(buf) + "</title>";
                    buf = rs.getString("description");
                    if (buf != null)
                        ret += "<description>" + StringEscapeUtils.escapeXml(buf) + "</description>";
                    buf = rs.getString("creator");
                    if (buf != null)
                        ret += "<creator>" + StringEscapeUtils.escapeXml(buf) + "</creator>";
                    buf = rs.getString("commit_note");
                    if (buf != null)
                        ret += "<commitNote>" + StringEscapeUtils.escapeXml(buf) + "</commitNote>";

                    // Default action.
                    defaultActionId = rs.getInt("default_action_id");
                    if (!rs.wasNull()) {
                        csiro.pidsvc.mappingstore.action.Descriptor action = getAction(defaultActionId);
                        ret += "<action>";
                        ret += "<type>" + action.Type + "</type>";
                        if (action.Name != null)
                            ret += "<name>" + StringEscapeUtils.escapeXml(action.Name) + "</name>";
                        if (action.Value != null)
                            ret += "<value>" + StringEscapeUtils.escapeXml(action.Value) + "</value>";
                        buf = rs.getString("default_action_description");
                        if (buf != null)
                            ret += "<description>" + StringEscapeUtils.escapeXml(buf) + "</description>";
                        ret += "</action>";
                    }

                    // Conditions.
                    ret += exportConditionsByMappingId(rs.getInt("mapping_id"));

                    ret += "</mapping>";
                }
            }

            // Condition sets.
            if (includeConditionSets)
                ret += exportConditionSetImpl(null);

            // Lookup maps.
            if (includeLookupMaps)
                ret += exportLookupImpl(null);
        } catch (Exception e) {
            _logger.error(e);
        } finally {
            if (rs != null)
                rs.close();
            if (pst != null)
                pst.close();
        }
        ret += "</backup>";
        return ret;
    }

    protected String exportConditionsByMappingId(int mappingId) throws SQLException {
        return exportConditions(getConditionsByMappingId(mappingId));
    }

    protected String exportConditionsBySetId(int conditionSetId) throws SQLException {
        return exportConditions(getConditionsBySetId(conditionSetId));
    }

    protected String exportConditions(Vector<csiro.pidsvc.mappingstore.condition.Descriptor> conditions)
            throws SQLException {
        String ret = "";
        List actions;

        if (conditions != null && conditions.size() > 0) {
            ret += "<conditions>";
            for (csiro.pidsvc.mappingstore.condition.Descriptor condition : conditions) {
                ret += "<condition>";
                ret += "<type>" + condition.Type + "</type>";
                ret += "<match>" + StringEscapeUtils.escapeXml(condition.Match) + "</match>";
                if (condition.Description != null)
                    ret += "<description>" + StringEscapeUtils.escapeXml(condition.Description) + "</description>";

                actions = getActionsByConditionId(condition.ID);
                if (actions != null && actions.size() > 0) {
                    ret += "<actions>";
                    for (csiro.pidsvc.mappingstore.action.Descriptor action : actions) {
                        ret += "<action>";
                        ret += "<type>" + action.Type + "</type>";
                        if (action.Name != null)
                            ret += "<name>" + StringEscapeUtils.escapeXml(action.Name) + "</name>";
                        if (action.Value != null)
                            ret += "<value>" + StringEscapeUtils.escapeXml(action.Value) + "</value>";
                        ret += "</action>";
                    }
                    ;
                    ret += "</actions>";
                }
                ret += "</condition>";
            }
            ret += "</conditions>";
        }
        return ret;
    }

    public String importMappings(HttpServletRequest request) {
        return unwrapCompressedBackupFile(request, new ICallback() {
            @Override
            public String process(InputStream inputStream) throws Exception {
                return createMapping(inputStream, true);
            }
        });
    }

    public String mergeMappingUpload(HttpServletRequest request) {
        final String mappingPath = request.getParameter("mapping_path");
        final String replace = request.getParameter("replace");

        return unwrapCompressedBackupFile(request, new ICallback() {
            @Override
            public String process(InputStream inputStream) throws Exception {
                String inputData = Stream.readInputStream(inputStream);
                return mergeMappingByPath(mappingPath, inputData, "1".equals(replace)).toString();
            }
        });
    }

    public boolean purgeDataStore() throws SQLException {
        PreparedStatement pst = null;
        try {
            pst = _connection.prepareStatement("TRUNCATE TABLE mapping RESTART IDENTITY CASCADE;");
            return pst.execute();
        } finally {
            if (pst != null)
                pst.close();
        }
    }

    /**************************************************************************
     *  Settings.
     */

    public boolean saveSettings(Map<?, ?> settings) throws SQLException {
        String sqlQuery = "BEGIN;\n", value;
        HashMap<String, String> params = new HashMap<String, String>();
        for (Object key : settings.keySet()) {
            if (key.equals("cmd"))
                continue;
            value = ((String[]) settings.get(key))[0];
            if (key.equals("BaseURI")) {
                try {
                    // Leave base URI only.
                    java.net.URI uri = new java.net.URI(value);
                    if (uri.getScheme() == null || uri.getHost() == null)
                        value = "";
                    else
                        value = uri.getScheme() + "://" + uri.getHost()
                                + (uri.getPort() == 80 || uri.getPort() == -1 ? "" : ":" + uri.getPort());
                } catch (URISyntaxException e) {
                    continue;
                }
            }
            params.put((String) key, value);
            sqlQuery += "UPDATE configuration SET value = ? WHERE name = ?;\n";
        }
        sqlQuery += "COMMIT;";

        // Execute SQL query.
        PreparedStatement pst = null;
        try {
            pst = _connection.prepareStatement(sqlQuery);

            // Set query parameters.
            int i = 1;
            for (String key : params.keySet()) {
                pst.setString(i++, params.get(key));
                pst.setString(i++, key);
            }

            return pst.execute();
        } finally {
            if (pst != null)
                pst.close();
        }
    }

    public String getSetting(String name) {
        PreparedStatement pst = null;
        ResultSet rs = null;

        try {
            pst = _connection.prepareStatement("SELECT value FROM configuration WHERE name = ?");
            pst.setString(1, name);

            if (pst.execute()) {
                rs = pst.getResultSet();
                if (rs.next())
                    return rs.getString(1);
            }
        } catch (Exception e) {
            _logger.error(e);
        } finally {
            try {
                if (rs != null)
                    rs.close();
                if (pst != null)
                    pst.close();
            } catch (SQLException e) {
                _logger.error(e);
            }
        }
        return null;
    }

    public String getBaseURI() {
        String baseURI = getSetting("BaseURI");
        if (baseURI == null) {
            try {
                // Execute SQL query.
                PreparedStatement pst = null;
                try {
                    pst = _connection.prepareStatement("BEGIN;\n"
                            + "DELETE FROM configuration WHERE name = 'BaseURI';\n"
                            + "INSERT INTO configuration (name, value) VALUES ('BaseURI', '');\n" + "COMMIT;");
                    pst.execute();
                    return "";
                } finally {
                    if (pst != null)
                        pst.close();
                }
            } catch (SQLException e) {
                _logger.error("Saving BaseURI setting failed.", e);
            }
        }
        return baseURI;
    }

    public boolean isCaseSensitive() {
        return _caseSensitivity.IsCaseSensitive;
    }

    /**************************************************************************
     *  Condition sets.
     */

    public String createConditionSet(InputStream inputStream)
            throws SaxonApiException, SQLException, IOException, ValidationException {
        return createConditionSet(Stream.readInputStream(inputStream));
    }

    public String createConditionSet(String inputData)
            throws SaxonApiException, SQLException, IOException, ValidationException {
        return processGenericXmlCommand(inputData, "xsd/backup.xsd", "xslt/import_conditionSet_postgresql.xslt");
    }

    public boolean deleteConditionSet(String name) throws SQLException {
        PreparedStatement pst = null;
        try {
            pst = _connection.prepareStatement("DELETE FROM condition_set WHERE name = ?;");
            pst.setString(1, name);
            return pst.execute();
        } finally {
            if (pst != null)
                pst.close();
        }
    }

    public int getConditionSetId(String name) throws SQLException {
        PreparedStatement pst = null;
        ResultSet rs = null;

        try {
            pst = _connection.prepareStatement("SELECT condition_set_id FROM condition_set WHERE name = ?;");
            pst.setString(1, name);

            if (pst.execute()) {
                rs = pst.getResultSet();
                if (rs.next())
                    return rs.getInt("condition_set_id");
            }
            return -1;
        } finally {
            if (rs != null)
                rs.close();
            if (pst != null)
                pst.close();
        }
    }

    public String importConditionSet(HttpServletRequest request) {
        return unwrapCompressedBackupFile(request, new ICallback() {
            @Override
            public String process(InputStream inputStream) throws Exception {
                return createConditionSet(inputStream);
            }
        });
    }

    public String exportConditionSet(String name) throws SQLException {
        String ret = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
        if (name == null)
            ret += "<backup xmlns=\"urn:csiro:xmlns:pidsvc:backup:1.0\">" + exportConditionSetImpl(null)
                    + "</backup>";
        else
            ret += exportConditionSetImpl(name);
        return ret;
    }

    protected String exportConditionSetImpl(String name) throws SQLException {
        PreparedStatement pst = null;
        ResultSet rs = null, rsMap = null;
        String ret = "";

        try {
            if (name == null) {
                // Export all condition sets.
                pst = _connection.prepareStatement("SELECT * FROM condition_set;");
            } else {
                // Export a particular condition set.
                pst = _connection.prepareStatement("SELECT * FROM condition_set WHERE name = ?;");
                pst.setString(1, name);
            }

            if (pst.execute()) {
                rs = pst.getResultSet();
                boolean dataAvailable = rs.next();

                // Backups may be empty. Otherwise throw an exception.
                if (name != null && !dataAvailable)
                    throw new SQLException("Condition set cannot be exported. Data may be corrupted.");

                if (dataAvailable) {
                    do {
                        String setName = rs.getString("name");
                        String setDesc = rs.getString("description");

                        ret += "<conditionSet xmlns=\"urn:csiro:xmlns:pidsvc:backup:1.0\">";
                        ret += "<name>" + StringEscapeUtils.escapeXml(setName) + "</name>";

                        if (setDesc != null && !setDesc.isEmpty())
                            ret += "<description>" + StringEscapeUtils.escapeXml(setDesc) + "</description>";

                        ret += exportConditionsBySetId(rs.getInt("condition_set_id"));
                        ret += "</conditionSet>";
                    } while (rs.next());
                }
            }
        } finally {
            if (rsMap != null)
                rsMap.close();
            if (rs != null)
                rs.close();
            if (pst != null)
                pst.close();
        }
        return ret;
    }

    /**************************************************************************
     *  Lookup maps.
     */

    public String createLookup(InputStream inputStream)
            throws SaxonApiException, SQLException, IOException, ValidationException {
        return createLookup(Stream.readInputStream(inputStream));
    }

    public String createLookup(String inputData)
            throws SaxonApiException, SQLException, IOException, ValidationException {
        return processGenericXmlCommand(inputData, "xsd/backup.xsd", "xslt/import_lookup_postgresql.xslt");
    }

    public boolean deleteLookup(String ns) throws SQLException {
        PreparedStatement pst = null;
        try {
            pst = _connection.prepareStatement("DELETE FROM lookup_ns WHERE ns = ?;");
            pst.setString(1, ns);
            return pst.execute();
        } finally {
            if (pst != null)
                pst.close();
        }
    }

    public LookupMapDescriptor getLookupMapType(String ns) {
        PreparedStatement pst = null;
        ResultSet rs = null;

        try {
            pst = _connection
                    .prepareStatement("SELECT type, behaviour_type, behaviour_value FROM lookup_ns WHERE ns = ?");
            pst.setString(1, ns);

            if (pst.execute()) {
                rs = pst.getResultSet();
                if (rs.next())
                    return new LookupMapDescriptor(rs.getString(1), rs.getString(2), rs.getString(3));
            }
        } catch (Exception e) {
            _logger.error(e);
        } finally {
            try {
                if (rs != null)
                    rs.close();
                if (pst != null)
                    pst.close();
            } catch (SQLException e) {
                _logger.error(e);
            }
        }
        return null;
    }

    public String getLookupValue(String ns, String key) {
        PreparedStatement pst = null;
        ResultSet rs = null;

        try {
            pst = _connection.prepareStatement("SELECT value FROM lookup WHERE ns = ? AND key = ? LIMIT 1");
            pst.setString(1, ns);
            pst.setString(2, key);

            if (pst.execute()) {
                rs = pst.getResultSet();
                if (rs.next())
                    return rs.getString(1);
            }
        } catch (Exception e) {
            _logger.error(e);
        } finally {
            try {
                if (rs != null)
                    rs.close();
                if (pst != null)
                    pst.close();
            } catch (SQLException e) {
                _logger.error(e);
            }
        }
        return null;
    }

    protected String[] getLookupKeyValue(String ns) {
        PreparedStatement pst = null;
        ResultSet rs = null;

        try {
            pst = _connection.prepareStatement("SELECT key, value FROM lookup WHERE ns = ? LIMIT 1");
            pst.setString(1, ns);

            if (pst.execute()) {
                rs = pst.getResultSet();
                if (rs.next())
                    return new String[] { rs.getString(1), rs.getString(2) };
            }
        } catch (Exception e) {
            _logger.error(e);
        } finally {
            try {
                if (rs != null)
                    rs.close();
                if (pst != null)
                    pst.close();
            } catch (SQLException e) {
                _logger.error(e);
            }
        }
        return null;
    }

    public String resolveLookupValue(String ns, String key) {
        LookupMapDescriptor lookupDescriptor = getLookupMapType(ns);
        if (lookupDescriptor == null)
            return null;

        try {
            if (lookupDescriptor.isStatic()) {
                String ret = getLookupValue(ns, key);
                return ret == null ? lookupDescriptor.getDefaultValue(key) : ret;
            } else if (lookupDescriptor.isHttpResolver()) {
                final Pattern reType = Pattern.compile("^T:(.+)$", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
                final Pattern reExtract = Pattern.compile("^E:(.+)$", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
                final Pattern reNamespace = Pattern.compile("^NS:(.+?):(.+)$",
                        Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
                Matcher m;
                String endpoint, extractorType, extractor, content;
                String[] dynKeyValue = getLookupKeyValue(ns);

                if (dynKeyValue == null)
                    return null;

                // Endpoint.
                endpoint = dynKeyValue[0];
                if (endpoint.contains("$0"))
                    endpoint = endpoint.replace("$0", key);
                else
                    endpoint += key;

                // Type.
                m = reType.matcher(dynKeyValue[1]);
                m.find();
                extractorType = m.group(1);

                // Extractor.
                m = reExtract.matcher(dynKeyValue[1]);
                m.find();
                extractor = m.group(1);

                // Execute HTTP GET request.
                content = Http.simpleGetRequestStrict(endpoint);
                if (content == null)
                    return lookupDescriptor.getDefaultValue(key);

                // Retrieve data.
                if (extractor.equals(""))
                    return content;
                if (extractorType.equalsIgnoreCase("Regex")) {
                    Pattern re = Pattern.compile(extractor);
                    m = re.matcher(content);

                    if (m.find())
                        return m.groupCount() > 0 ? m.group(1) : m.group();
                } else if (extractorType.equalsIgnoreCase("XPath")) {
                    Processor processor = new Processor(false);
                    XPathCompiler xpathCompiler = processor.newXPathCompiler();

                    // Declare XML namespaces.
                    m = reNamespace.matcher(dynKeyValue[1]);
                    while (m.find())
                        xpathCompiler.declareNamespace(m.group(1), m.group(2));

                    // Evaluate XPath expression.
                    XdmItem node = xpathCompiler.evaluateSingle(extractor,
                            processor.newDocumentBuilder().build(new StreamSource(new StringReader(content))));
                    return node == null ? lookupDescriptor.getDefaultValue(key) : node.getStringValue();
                }
            }
        } catch (Exception e) {
            _logger.error(e);
        }
        return lookupDescriptor.getDefaultValue(key);
    }

    public String exportLookup(String ns) throws SQLException {
        String ret = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
        if (ns == null)
            ret += "<backup xmlns=\"urn:csiro:xmlns:pidsvc:backup:1.0\">" + exportLookupImpl(null) + "</backup>";
        else
            ret += exportLookupImpl(ns);
        return ret;
    }

    protected String exportLookupImpl(String ns) throws SQLException {
        PreparedStatement pst = null;
        ResultSet rs = null, rsMap = null;
        String ret = "";

        try {
            if (ns == null) {
                // Export all lookup maps.
                pst = _connection
                        .prepareStatement("SELECT ns, type, behaviour_type, behaviour_value FROM lookup_ns;");
            } else {
                // Export a particular lookup map.
                pst = _connection.prepareStatement(
                        "SELECT ns, type, behaviour_type, behaviour_value FROM lookup_ns WHERE ns = ?;");
                pst.setString(1, ns);
            }

            if (pst.execute()) {
                rs = pst.getResultSet();
                boolean dataAvailable = rs.next();

                // Backups may be empty. Otherwise throw an exception.
                if (ns != null && !dataAvailable)
                    throw new SQLException("Lookup map configuration cannot be exported. Data may be corrupted.");

                if (dataAvailable) {
                    do {
                        String lookupNamespace = rs.getString("ns");
                        String lookupType = rs.getString("type");

                        ret += "<lookup xmlns=\"urn:csiro:xmlns:pidsvc:backup:1.0\">";
                        ret += "<ns>" + StringEscapeUtils.escapeXml(lookupNamespace) + "</ns>";

                        String behaviourValue = rs.getString("behaviour_value");
                        ret += "<default type=\"" + StringEscapeUtils.escapeXml(rs.getString("behaviour_type"))
                                + "\">"
                                + (behaviourValue == null ? "" : StringEscapeUtils.escapeXml(behaviourValue))
                                + "</default>";

                        pst = _connection.prepareStatement("SELECT key, value FROM lookup WHERE ns = ?;");
                        pst.setString(1, lookupNamespace);
                        if (!pst.execute())
                            throw new SQLException(
                                    "Lookup map configuration cannot be exported. Data may be corrupted.");
                        rsMap = pst.getResultSet();
                        if (lookupType.equalsIgnoreCase("Static")) {
                            ret += "<Static>";
                            while (rsMap.next()) {
                                ret += "<pair>";
                                ret += "<key>" + StringEscapeUtils.escapeXml(rsMap.getString(1)) + "</key>";
                                ret += "<value>" + StringEscapeUtils.escapeXml(rsMap.getString(2)) + "</value>";
                                ret += "</pair>";
                            }
                            ret += "</Static>";
                        } else if (lookupType.equalsIgnoreCase("HttpResolver")) {
                            ret += "<HttpResolver>";
                            if (!rsMap.next())
                                throw new SQLException(
                                        "Lookup map configuration cannot be exported. Data is corrupted.");

                            final Pattern reType = Pattern.compile("^T:(.+)$",
                                    Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
                            final Pattern reExtract = Pattern.compile("^E:(.+)$",
                                    Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
                            final Pattern reNamespace = Pattern.compile("^NS:(.+?):(.+)$",
                                    Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
                            Matcher m;
                            String namespaces = "";
                            String buf = rsMap.getString(2);

                            ret += "<endpoint>" + StringEscapeUtils.escapeXml(rsMap.getString(1)) + "</endpoint>";

                            // Type.
                            m = reType.matcher(buf);
                            m.find();
                            ret += "<type>" + m.group(1) + "</type>";

                            // Extractor.
                            m = reExtract.matcher(buf);
                            m.find();
                            ret += "<extractor>" + StringEscapeUtils.escapeXml(m.group(1)) + "</extractor>";

                            // Namespaces.
                            m = reNamespace.matcher(buf);
                            while (m.find())
                                namespaces += "<ns prefix=\"" + StringEscapeUtils.escapeXml(m.group(1)) + "\">"
                                        + StringEscapeUtils.escapeXml(m.group(2)) + "</ns>";
                            if (!namespaces.isEmpty())
                                ret += "<namespaces>" + namespaces + "</namespaces>";

                            ret += "</HttpResolver>";
                        }
                        ret += "</lookup>";
                    } while (rs.next());
                }
            }
        } finally {
            if (rsMap != null)
                rsMap.close();
            if (rs != null)
                rs.close();
            if (pst != null)
                pst.close();
        }
        return ret;
    }

    public String importLookup(HttpServletRequest request) {
        return unwrapCompressedBackupFile(request, new ICallback() {
            @Override
            public String process(InputStream inputStream) throws Exception {
                return createLookup(inputStream);
            }
        });
    }

    /**************************************************************************
     *  QR Codes.
     */

    public boolean increaseQrCodeHitCounter(int mappingId) throws SQLException {
        PreparedStatement pst = null;
        try {
            pst = _connection.prepareStatement("UPDATE mapping SET qr_hits = qr_hits + 1 WHERE mapping_id = ?;");
            pst.setInt(1, mappingId);
            return pst.execute();
        } finally {
            if (pst != null)
                pst.close();
        }
    }

    public int getTotalQrCodeHits(String mappingPath) throws SQLException {
        PreparedStatement pst = null;
        try {
            pst = _connection.prepareStatement("SELECT SUM(qr_hits) FROM mapping WHERE mapping_path = ?;");
            pst.setString(1, mappingPath);
            if (pst.execute()) {
                ResultSet rs = pst.getResultSet();
                if (rs.next())
                    return rs.getInt(1);
            }
        } finally {
            if (pst != null)
                pst.close();
        }
        return 0;
    }
}