org.fao.geonet.component.csw.GetRecords.java Source code

Java tutorial

Introduction

Here is the source code for org.fao.geonet.component.csw.GetRecords.java

Source

//=============================================================================
//===   Copyright (C) 2001-2007 Food and Agriculture Organization of the
//===   United Nations (FAO-UN), United Nations World Food Programme (WFP)
//===   and United Nations Environment Programme (UNEP)
//===
//===   This program is free software; you can redistribute it and/or modify
//===   it under the terms of the GNU General Public License as published by
//===   the Free Software Foundation; either version 2 of the License, or (at
//===   your option) any later version.
//===
//===   This program is distributed in the hope that it will be useful, but
//===   WITHOUT ANY WARRANTY; without even the implied warranty of
//===   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
//===   General Public License for more details.
//===
//===   You should have received a copy of the GNU General Public License
//===   along with this program; if not, write to the Free Software
//===   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
//===
//===   Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
//===   Rome - Italy. email: geonetwork@osgeo.org
//==============================================================================

package org.fao.geonet.component.csw;

import jeeves.server.context.ServiceContext;

import org.apache.commons.lang.StringUtils;
import org.apache.lucene.search.Sort;
import org.fao.geonet.GeonetContext;
import org.fao.geonet.constants.Geonet;
import org.fao.geonet.csw.common.ConstraintLanguage;
import org.fao.geonet.csw.common.Csw;
import org.fao.geonet.csw.common.ElementSetName;
import org.fao.geonet.csw.common.OutputSchema;
import org.fao.geonet.csw.common.ResultType;
import org.fao.geonet.csw.common.exceptions.CatalogException;
import org.fao.geonet.csw.common.exceptions.InvalidParameterValueEx;
import org.fao.geonet.csw.common.exceptions.MissingParameterValueEx;
import org.fao.geonet.csw.common.exceptions.NoApplicableCodeEx;
import org.fao.geonet.domain.CustomElementSet;
import org.fao.geonet.domain.ISODate;
import org.fao.geonet.domain.Pair;
import org.fao.geonet.kernel.DataManager;
import org.fao.geonet.kernel.SchemaManager;
import org.fao.geonet.kernel.csw.CatalogConfiguration;
import org.fao.geonet.kernel.csw.CatalogService;
import org.fao.geonet.kernel.csw.services.AbstractOperation;
import org.fao.geonet.kernel.csw.services.getrecords.FieldMapper;
import org.fao.geonet.kernel.csw.services.getrecords.SearchController;
import org.fao.geonet.kernel.search.LuceneSearcher;
import org.fao.geonet.kernel.search.SearchManager;
import org.fao.geonet.kernel.setting.SettingInfo;
import org.fao.geonet.kernel.setting.SettingManager;
import org.fao.geonet.repository.CustomElementSetRepository;
import org.fao.geonet.util.xml.NamespaceUtils;
import org.fao.geonet.utils.Log;
import org.fao.geonet.utils.Xml;
import org.jdom.Attribute;
import org.jdom.Element;
import org.jdom.Namespace;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.nio.file.Path;
import java.util.*;

import static org.fao.geonet.kernel.setting.Settings.SYSTEM_CSW_ENABLEWHENINDEXING;

/**
 * See OGC 07-006 and OGC 07-045.
 */
@Component(CatalogService.BEAN_PREFIX + GetRecords.NAME)
public class GetRecords extends AbstractOperation implements CatalogService {

    static final String NAME = "GetRecords";

    /**
     * OGC 07-006 10.8.4.4.
     */
    private static final String defaultOutputFormat = "application/xml";

    //---------------------------------------------------------------------------
    //---
    //--- Constructor
    //---
    //---------------------------------------------------------------------------

    private SearchController _searchController;
    @Autowired
    private CatalogConfiguration _catalogConfig;
    @Autowired
    private FieldMapper _fieldMapper;

    @Autowired
    private SchemaManager _schemaManager;

    @Autowired
    public GetRecords(ApplicationContext context) {
        _searchController = new SearchController(context);
    }

    /**
     * @return
     */
    public SearchController getSearchController() {
        return _searchController;
    }

    ;

    //---------------------------------------------------------------------------
    //---
    //--- API methods
    //---
    //---------------------------------------------------------------------------
    public String getName() {
        return NAME;
    }

    /**
     *
     * @param request
     * @param context
     * @return
     * @throws CatalogException
     */
    public Element execute(Element request, ServiceContext context) throws CatalogException {
        String timeStamp = new ISODate().toString();

        // Return exception is indexing.
        GeonetContext gc = (GeonetContext) context.getHandlerContext(Geonet.CONTEXT_NAME);
        DataManager dataManager = gc.getBean(DataManager.class);
        SettingManager settingsManager = gc.getBean(SettingManager.class);
        if (!settingsManager.getValueAsBool(SYSTEM_CSW_ENABLEWHENINDEXING) && dataManager.isIndexing()) {
            throw new RuntimeException("Catalog is indexing records, retry later.");
        }

        //
        // some validation checks (note: this is not an XSD validation)
        //

        // must be "CSW"
        checkService(request);

        // must be "2.0.2"
        checkVersion(request);

        // GeoNetwork only supports "application/xml"
        checkOutputFormat(request);

        // one of ElementName XOR ElementSetName must be requested
        checkElementNamesXORElementSetName(request);

        // optional integer, value at least 1
        int startPos = getStartPosition(request);

        // optional integer, value at least 1
        int maxRecords = getMaxRecords(request);

        Element query = request.getChild("Query", Csw.NAMESPACE_CSW);

        // one of "hits", "results", "validate" or the GN-specific (invalid) "results_with_summary".
        ResultType resultType = ResultType.parse(request.getAttributeValue("resultType"));

        // either Record or IsoRecord
        String outSchema = OutputSchema.parse(request.getAttributeValue("outputSchema"), _schemaManager);

        // GeoNetwork-specific parameter defining how to deal with ElementNames. See documentation in
        // SearchController.applyElementNames() about these strategies.
        String elementnameStrategy = getElementNameStrategy(query);

        // value csw:Record or gmd:MD_Metadata
        // heikki: this is actually a List (in OGC 07-006), though OGC 07-045 states:
        // "Because for this application profile it is not possible that a query includes more than one typename, .."
        // So: a single String should be OK. However to increase backwards-compatibility with existing clients sending
        // a comma separated list (such as GN's own CSW Harvesting Client), the check assumes a comma-separated list,
        // and checks whether its values are not other than csw:Record or gmd:MD_Metadata. If both are sent,
        // gmd:MD_Metadata is preferred.
        final SettingInfo settingInfo = context.getBean(SearchManager.class).getSettingInfo();
        String typeName = checkTypenames(query, settingInfo.getInspireEnabled());

        // set of elementnames or null
        Set<String> elemNames = getElementNames(query);

        // If any element names are specified, it's an ad hoc query and overrides the element set name default. In that
        // case, we set setName to FULL instead of SUMMARY so that we can retrieve a CSW:Record and trim out the
        // elements that aren't in the elemNames set.
        ElementSetName setName = ElementSetName.FULL;

        //
        // no ElementNames requested: use ElementSetName
        //
        if ((elemNames == null)) {
            setName = getElementSetName(query, ElementSetName.SUMMARY);
            // elementsetname is FULL: use customized elementset if defined
            if (setName.equals(ElementSetName.FULL)) {
                final List<CustomElementSet> customElementSets = context.getBean(CustomElementSetRepository.class)
                        .findAll();
                // custom elementset defined
                if (!CollectionUtils.isEmpty(customElementSets)) {
                    elemNames = new HashSet<String>();
                    for (CustomElementSet customElementSet : customElementSets) {
                        elemNames.add(customElementSet.getXpath());
                    }
                }
            }
        }

        // "Constraint Optional" & "Must be specified with QUERYCONSTRAINT parameter"
        Element constr = query.getChild("Constraint", Csw.NAMESPACE_CSW);
        Element filterExpr = getFilterExpression(constr);
        String filterVersion = getFilterVersion(constr);

        // Get max hits to be used for summary - CSW GeoNetwork extension
        int maxHitsInSummary = 1000;
        String sMaxRecordsInKeywordSummary = query.getAttributeValue("maxHitsInSummary");
        if (sMaxRecordsInKeywordSummary != null) {
            // TODO : it could be better to use service config parameter instead
            // sMaxRecordsInKeywordSummary = config.getValue("maxHitsInSummary", "1000");
            maxHitsInSummary = Integer.parseInt(sMaxRecordsInKeywordSummary);
        }

        Sort sort = getSortFields(request, context);

        Element response;

        if (resultType == ResultType.VALIDATE) {
            //String schema = context.getAppPath() + Geonet.Path.VALIDATION + "csw/2.0.2/csw-2.0.2.xsd";
            Path schema = context.getAppPath().resolve(Geonet.Path.VALIDATION)
                    .resolve("csw202_apiso100/csw/2.0.2/CSW-discovery.xsd");

            if (Log.isDebugEnabled(Geonet.CSW))
                Log.debug(Geonet.CSW, "Validating request against " + schema);
            try {
                Xml.validate(schema, request);
            } catch (Exception e) {
                throw new NoApplicableCodeEx("Request failed validation:" + e.toString());
            }

            response = new Element("Acknowledgement", Csw.NAMESPACE_CSW);
            response.setAttribute("timeStamp", timeStamp);

            Element echoedRequest = new Element("EchoedRequest", Csw.NAMESPACE_CSW);
            echoedRequest.addContent(request);

            response.addContent(echoedRequest);
        } else {
            response = new Element(getName() + "Response", Csw.NAMESPACE_CSW);

            Attribute schemaLocation = new Attribute("schemaLocation",
                    "http://www.opengis.net/cat/csw/2.0.2 http://schemas.opengis.net/csw/2.0.2/CSW-discovery.xsd",
                    Csw.NAMESPACE_XSI);
            response.setAttribute(schemaLocation);

            Element status = new Element("SearchStatus", Csw.NAMESPACE_CSW);
            status.setAttribute("timestamp", timeStamp);

            response.addContent(status);

            String cswServiceSpecificContraint = request.getChildText(Geonet.Elem.FILTER);

            Pair<Element, Element> search = _searchController.search(context, startPos, maxRecords, resultType,
                    outSchema, setName, filterExpr, filterVersion, sort, elemNames, typeName, maxHitsInSummary,
                    cswServiceSpecificContraint, elementnameStrategy);

            // Only add GeoNetwork summary on results_with_summary option
            if (resultType == ResultType.RESULTS_WITH_SUMMARY) {
                response.addContent(search.one());
            }

            response.addContent(search.two());
        }
        return response;
    }

    /**
     * TODO javadoc.
     */
    public Element adaptGetRequest(Map<String, String> params) throws CatalogException {
        String service = params.get("service");
        String version = params.get("version");
        String resultType = params.get("resulttype");
        String outputFormat = params.get("outputformat");
        String outputSchema = params.get("outputschema");
        String startPosition = params.get("startposition");
        String maxRecords = params.get("maxrecords");
        String hopCount = params.get("hopcount");
        String distribSearch = params.get("distributedsearch");
        String typeNames = params.get("typenames");
        String elemSetName = params.get("elementsetname");
        String elemName = params.get("elementname");
        String constraint = params.get("constraint");
        String constrLang = params.get("constraintlanguage");
        String constrLangVer = params.get("constraint_language_version");
        String sortby = params.get("sortby");
        String elementnameStrategy = params.get("elementnamestrategy");

        //--- build POST request

        Element request = new Element(getName(), Csw.NAMESPACE_CSW);

        setAttrib(request, "service", service);
        setAttrib(request, "version", version);
        setAttrib(request, "resultType", resultType);
        setAttrib(request, "outputFormat", outputFormat);
        setAttrib(request, "outputSchema", outputSchema);
        setAttrib(request, "startPosition", startPosition);
        setAttrib(request, "maxRecords", maxRecords);

        if (distribSearch != null && distribSearch.equals("true")) {
            Element ds = new Element("DistributedSearch", Csw.NAMESPACE_CSW);
            ds.setText("TRUE");

            if (hopCount != null) {
                ds.setAttribute("hopCount", hopCount);
            }
            request.addContent(ds);
        }

        //--- build query element

        Element query = new Element("Query", Csw.NAMESPACE_CSW);
        request.addContent(query);

        if (elementnameStrategy != null) {
            setAttrib(query, "elementnamestrategy", elementnameStrategy);
        }

        if (typeNames != null) {
            setAttrib(query, "typeNames", typeNames.replace(',', ' '));
        }
        //--- these 2 are in mutual exclusion

        addElement(query, "ElementSetName", elemSetName);
        fill(query, "ElementName", elemName);

        //--- handle constraint

        if (constraint != null) {
            ConstraintLanguage language = ConstraintLanguage.parse(constrLang);
            Element constr = new Element("Constraint", Csw.NAMESPACE_CSW);
            query.addContent(constr);

            if (language == ConstraintLanguage.CQL) {
                addElement(constr, "CqlText", constraint);
            } else {
                try {
                    constr.addContent(Xml.loadString(constraint, false));
                } catch (Exception e) {
                    e.printStackTrace();
                    throw new NoApplicableCodeEx("Constraint is not a valid xml");
                }
            }
            setAttrib(constr, "version", constrLangVer);
        }

        //--- handle sortby

        if (sortby != null) {
            Element sortBy = new Element("SortBy", Csw.NAMESPACE_OGC);
            query.addContent(sortBy);

            StringTokenizer st = new StringTokenizer(sortby, ",");
            while (st.hasMoreTokens()) {
                String sortInfo = st.nextToken();
                String field = sortInfo.substring(0, sortInfo.length() - 2);
                boolean ascen = sortInfo.endsWith(":A");

                Element sortProp = new Element("SortProperty", Csw.NAMESPACE_OGC);
                sortBy.addContent(sortProp);

                Element propName = new Element("PropertyName", Csw.NAMESPACE_OGC).setText(field);
                Element sortOrder = new Element("SortOrder", Csw.NAMESPACE_OGC).setText(ascen ? "ASC" : "DESC");

                sortProp.addContent(propName);
                sortProp.addContent(sortOrder);
            }
        }

        return request;
    }

    /**
     * TODO javadoc.
     */
    public Element retrieveValues(String parameterName) throws CatalogException {

        Element listOfValues = null;
        if (parameterName.equalsIgnoreCase("resultType") || parameterName.equalsIgnoreCase("outputFormat")
                || parameterName.equalsIgnoreCase("elementSetName")
                || parameterName.equalsIgnoreCase("outputSchema") || parameterName.equalsIgnoreCase("typenames"))
            listOfValues = new Element("ListOfValues", Csw.NAMESPACE_CSW);

        // Handle resultType parameter
        if (parameterName.equalsIgnoreCase("resultType")) {
            List<Element> values = new ArrayList<Element>();
            ResultType[] resultType = ResultType.values();
            for (ResultType aResultType : resultType) {
                String value = aResultType.toString();
                values.add(new Element("Value", Csw.NAMESPACE_CSW).setText(value));
            }
            if (listOfValues != null) {
                listOfValues.addContent(values);
            }
        }

        // Handle elementSetName parameter
        if (parameterName.equalsIgnoreCase("elementSetName")) {
            List<Element> values = new ArrayList<Element>();
            ElementSetName[] esn = ElementSetName.values();
            for (ElementSetName anEsn : esn) {
                String value = anEsn.toString();
                values.add(new Element("Value", Csw.NAMESPACE_CSW).setText(value));
            }
            if (listOfValues != null) {
                listOfValues.addContent(values);
            }
        }

        // Handle outputFormat parameter
        if (parameterName.equalsIgnoreCase("outputformat")) {
            Set<String> formats = _catalogConfig.getGetRecordsOutputFormat();
            List<Element> values = createValuesElement(formats);
            if (listOfValues != null) {
                listOfValues.addContent(values);
            }
        }

        // Handle outputSchema parameter
        if (parameterName.equalsIgnoreCase("outputSchema")) {
            Set<String> namespacesUri = _catalogConfig.getGetRecordsOutputSchema();
            List<Element> values = createValuesElement(namespacesUri);
            if (listOfValues != null) {
                listOfValues.addContent(values);
            }
        }

        // Handle typenames parameter
        if (parameterName.equalsIgnoreCase("typenames")) {
            Set<String> typenames = _catalogConfig.getGetRecordsTypenames();
            List<Element> values = createValuesElement(typenames);
            if (listOfValues != null) {
                listOfValues.addContent(values);
            }
        }

        return listOfValues;
    }

    //---------------------------------------------------------------------------
    //---
    //--- Private methods
    //---
    //---------------------------------------------------------------------------

    /**
     * GeoNetwork only supports default value for outputFormat.
     *
     * OGC 07-006: The only value that is required to be supported is application/xml. Other
     * supported values may include text/html and text/plain. Cardinality: Zero or one (Optional).
     * Default value is application/xml.
     *
     * In the case where the output format is application/xml, the CSW shall generate an XML
     * document that validates against a schema document that is specified in the output document
     * via the xsi:schemaLocation attribute defined in XML.
     *
     * @param request GetRecords request
     * @throws InvalidParameterValueEx hmm
     */
    private String checkOutputFormat(Element request) throws InvalidParameterValueEx {
        String format = request.getAttributeValue("outputFormat");
        if (format != null && !format.equals(defaultOutputFormat)) {
            throw new InvalidParameterValueEx("outputFormat", format);
        } else {
            return defaultOutputFormat;
        }
    }

    /**
     * If the request contains a Query element, it must have attribute typeNames.
     *
     * The OGC 07-045 spec is more restrictive than OGC 07-006.
     *
     * OGC 07-006 10.8.4.8: The typeNames parameter is a list of one or more names of queryable
     * entities in the catalogue's information model that may be constrained in the predicate of the
     * query. In the case of XML realization of the OGC core metadata properties (Subclause 10.2.5),
     * the element csw:Record is the only queryable entity. Other information models may include
     * more than one queryable component. For example, queryable components for the XML realization
     * of the ebRIM include rim:Service, rim:ExtrinsicObject and rim:Association. In such cases the
     * application profile shall describe how multiple typeNames values should be processed. In
     * addition, all or some of the these queryable entity names may be specified in the query to
     * define which metadata record elements the query should present in the response to the
     * GetRecords operation.
     *
     * OGC 07-045 8.2.2.1.1: Mandatory: Must support *one* of csw:Record or
     * gmd:MD_Metadata in a query. Default value is csw:Record.
     *
     * (note how OGC 07-045 mixes up a mandatory parameter that has a default value !!)
     *
     * We'll go for the default value option rather than the mandatory-ness. So: if typeNames is not
     * present or empty, "csw:Record" is used.
     *
     * If the request does not contain exactly one (or comma-separated, both) of the values
     * specified in OGC 07-045, an exception is thrown. If both are present "gmd:MD_Metadata" is
     * preferred.
     *
     * @param query    query element
     * @param isStrict enable strict error message to comply with GDI-DE Testsuite test
     *                 csw:InterfaceBindings.GetRecords-InvalidRequest
     * @return typeName
     * @throws MissingParameterValueEx if typeNames is missing
     * @throws InvalidParameterValueEx if typeNames does not have one of the mandated values
     */
    private String checkTypenames(Element query, boolean isStrict)
            throws MissingParameterValueEx, InvalidParameterValueEx {
        if (Log.isDebugEnabled(Geonet.CSW_SEARCH)) {
            Log.debug(Geonet.CSW_SEARCH, "checking typenames in query:\n" + Xml.getString(query));
        }
        //
        // get the prefix used for CSW namespace used in this input document
        //
        String cswPrefix = getPrefixForNamespace(query, Csw.NAMESPACE_CSW);
        if (cswPrefix == null) {
            if (Log.isDebugEnabled(Geonet.CSW_SEARCH)) {
                Log.debug(Geonet.CSW_SEARCH,
                        "checktypenames: csw prefix not found, using " + Csw.NAMESPACE_CSW.getPrefix());
            }
            cswPrefix = Csw.NAMESPACE_CSW.getPrefix();
        }
        //
        // get the prefix used for GMD namespace used in this input document
        //
        String gmdPrefix = getPrefixForNamespace(query, Csw.NAMESPACE_GMD);
        if (gmdPrefix == null) {
            if (Log.isDebugEnabled(Geonet.CSW_SEARCH)) {
                Log.debug(Geonet.CSW_SEARCH,
                        "checktypenames: gmd prefix not found, using " + Csw.NAMESPACE_GMD.getPrefix());
            }
            gmdPrefix = Csw.NAMESPACE_GMD.getPrefix();
        }
        if (Log.isDebugEnabled(Geonet.CSW_SEARCH)) {
            Log.debug(Geonet.CSW_SEARCH,
                    "checktypenames: csw prefix set to " + cswPrefix + ", gmd prefix set to " + gmdPrefix);
        }

        Attribute typeNames = query.getAttribute("typeNames", query.getNamespace());
        typeNames = query.getAttribute("typeNames");
        if (typeNames != null) {
            String typeNamesValue = typeNames.getValue();
            // empty typenames element
            if (StringUtils.isEmpty(typeNamesValue)) {
                return cswPrefix + ":Record";
            }
            // not empty: scan space-separated string
            @SuppressWarnings("resource")
            Scanner spaceScanner = new Scanner(typeNamesValue);
            spaceScanner.useDelimiter(" ");
            String result = cswPrefix + ":Record";
            while (spaceScanner.hasNext()) {
                String typeName = spaceScanner.next();
                typeName = typeName.trim();
                if (Log.isDebugEnabled(Geonet.CSW_SEARCH)) {
                    Log.debug(Geonet.CSW_SEARCH, "checking typename in query:" + typeName);
                }

                if (!_schemaManager.getListOfTypeNames().contains(typeName)) {
                    throw new InvalidParameterValueEx("typeNames",
                            String.format("'%s' typename is not valid. Supported values are: %s", typeName,
                                    _schemaManager.getListOfTypeNames()));
                }
                if (typeName.equals(gmdPrefix + ":MD_Metadata")) {
                    return typeName;
                }
            }
            return result;
        }
        // missing typeNames element
        else {
            if (isStrict) {
                //Mandatory check if strict.
                throw new MissingParameterValueEx("typeNames", String.format(
                        "Attribute 'typeNames' is missing. Supported values are: %s. Default is csw:Record according to OGC 07-045.",
                        _schemaManager.getListOfTypeNames()));
            } else {
                //Return default value according to OGC 07-045.
                return cswPrefix + ":Record";
            }
        }
    }

    /**
     * Returns the prefix used in the scope of an element for a particular namespace, or null if the
     * namespace is not in scope.
     */
    private String getPrefixForNamespace(Element element, Namespace namespace) {
        List<Namespace> namespacesInScope = NamespaceUtils.getNamespacesInScope(element);
        for (Namespace ns : namespacesInScope) {
            if (ns.getURI().equals(namespace.getURI())) {
                return ns.getPrefix();
            }
        }
        return null;
    }

    /**
     * GeoNetwork-specific parameter to control the behaviour when dealing with ElementNames.
     * Supported values are 'csw202', 'relaxed' , 'context'  and 'geonetwork26'. If the parameter is
     * missing or does not have one of these values, the default value 'relaxed' is used. See
     * documentation in SearchController.applyElementNames() about these strategies.
     *
     * @param query query element
     * @return elementnames strategy
     */
    private String getElementNameStrategy(Element query) {
        if (Log.isDebugEnabled(Geonet.CSW_SEARCH))
            Log.debug(Geonet.CSW_SEARCH, "getting elementnameStrategy from query:\n" + Xml.getString(query));
        Attribute elementNameStrategyA = query.getAttribute("elementnameStrategy");
        // default
        String elementNameStrategy = "relaxed";
        if (elementNameStrategyA != null) {
            elementNameStrategy = elementNameStrategyA.getValue();
        }
        // empty or not one of the supported values
        if (StringUtils.isNotEmpty(elementNameStrategy)
                && !(elementNameStrategy.equals("csw202") || elementNameStrategy.equals("relaxed")
                        || elementNameStrategy.equals("context") || elementNameStrategy.equals("geonetwork26"))) {
            // use default
            elementNameStrategy = "relaxed";
        }
        if (Log.isDebugEnabled(Geonet.CSW_SEARCH))
            Log.debug(Geonet.CSW_SEARCH, "elementNameStrategy: " + elementNameStrategy);
        return elementNameStrategy;
    }

    /**
     * Checks that not ElementName and ElementSetName are both present in the query, see OGC 07-006
     * section 10 8 4 9.
     */
    private void checkElementNamesXORElementSetName(Element request) throws InvalidParameterValueEx {
        Element query = request.getChild("Query", Csw.NAMESPACE_CSW);
        if (query != null) {
            boolean elementNamePresent = !CollectionUtils
                    .isEmpty(query.getChildren("ElementName", query.getNamespace()));
            boolean elementSetNamePresent = !CollectionUtils
                    .isEmpty(query.getChildren("ElementSetName", query.getNamespace()));
            if (elementNamePresent && elementSetNamePresent) {
                throw new InvalidParameterValueEx("ElementName and ElementSetName", "mutually exclusive");
            }
        }
    }

    /**
     * OGC 07-006 and OGC 07-045: Non-zero, positive Integer. Optional. The default value is 1.
     *
     * @param request the request
     * @return startPosition
     * @throws InvalidParameterValueEx hmm
     */
    private int getStartPosition(Element request) throws InvalidParameterValueEx {
        String start = request.getAttributeValue("startPosition");
        if (start == null) {
            return 1;
        }
        try {
            int value = Integer.parseInt(start);
            if (value >= 1) {
                return value;
            } else {
                throw new InvalidParameterValueEx("startPosition", start);
            }
        } catch (NumberFormatException x) {
            throw new InvalidParameterValueEx("startPosition", start);
        }
    }

    /**
     * OGC 07-006 and OGC 07-045: PositiveInteger. Optional. The default value is 10.
     *
     * @param request the request
     * @return maxRecords
     * @throws InvalidParameterValueEx hmm
     */
    private int getMaxRecords(Element request) throws InvalidParameterValueEx {
        String max = request.getAttributeValue("maxRecords");
        if (max == null) {
            return 10;
        }
        try {
            int value = Integer.parseInt(max);
            if (value >= 1) {
                return value;
            } else {
                throw new InvalidParameterValueEx("maxRecords", max);
            }
        } catch (NumberFormatException x) {
            throw new InvalidParameterValueEx("maxRecords", max);
        }
    }

    /**
     * a return value >= 0 means that a distributed search was requested, otherwise the method returns -1.
     *
     * TODO unused method
     *
     * @param request
     * @return
     * @throws InvalidParameterValueEx
     */
    //    private int getHopCount(Element request) throws InvalidParameterValueEx {
    //        Element ds = request.getChild("DistributedSearch", Csw.NAMESPACE_CSW);
    //        if (ds == null) {
    //            return -1;
    //        }
    //        String hopCount = ds.getAttributeValue("hopCount");
    //        if (hopCount == null) {
    //            return 2;
    //        }
    //        try {
    //            int value = Integer.parseInt(hopCount);
    //            if (value >= 0) {
    //                return value;
    //            }
    //        }
    //        catch (NumberFormatException ignored) {
    //            throw new InvalidParameterValueEx("hopCount", hopCount);
    //        }
    //        throw new InvalidParameterValueEx("hopCount", hopCount);
    //    }

    /**
     * TODO javadoc.
     */
    private Sort getSortFields(Element request, ServiceContext context) {
        Element query = request.getChild("Query", Csw.NAMESPACE_CSW);
        if (query == null) {
            return null;
        }

        Element sortBy = query.getChild("SortBy", Csw.NAMESPACE_OGC);
        if (sortBy == null) {
            return null;
        }

        @SuppressWarnings("unchecked")
        List<Element> list = sortBy.getChildren();
        List<Pair<String, Boolean>> sortFields = new ArrayList<Pair<String, Boolean>>();
        for (Element el : list) {
            String field = el.getChildText("PropertyName", Csw.NAMESPACE_OGC);
            String order = el.getChildText("SortOrder", Csw.NAMESPACE_OGC);

            // Map CSW search field to Lucene for sorting. And if not mapped assumes the field is a Lucene field.
            String luceneField = _fieldMapper.map(field);
            if (luceneField != null) {
                sortFields.add(Pair.read(luceneField, "DESC".equals(order)));
            } else {
                sortFields.add(Pair.read(field, "DESC".equals(order)));
            }
        }

        GeonetContext gc = (GeonetContext) context.getHandlerContext(Geonet.CONTEXT_NAME);
        SearchManager sm = gc.getBean(SearchManager.class);
        boolean requestedLanguageOnTop = sm.getSettingInfo().getRequestedLanguageOnTop();

        String preferredLanguage = LuceneSearcher.determineLanguage(context, request,
                sm.getSettingInfo()).presentationLanguage;

        // we always want to keep the relevancy as part of the sorting mechanism
        return LuceneSearcher.makeSort(sortFields, preferredLanguage, requestedLanguageOnTop);
    }

    /**
     * Returns the values of ElementNames in the query, or null if there are none.
     *
     * @param query the query
     * @return set of elementname values
     */
    private Set<String> getElementNames(Element query) {
        if (Log.isDebugEnabled(Geonet.CSW))
            Log.debug(Geonet.CSW, "GetRecords getElementNames");
        Set<String> elementNames = null;
        if (query != null) {
            @SuppressWarnings("unchecked")
            List<Element> elementList = query.getChildren("ElementName", query.getNamespace());
            for (Element element : elementList) {
                if (elementNames == null) {
                    elementNames = new HashSet<String>();
                }
                elementNames.add(element.getTextNormalize());
            }
        }
        // TODO in if(isDebugEnabled) condition. Jeeves LOG doesn't provide that useful function though.
        if (elementNames != null) {
            for (String elementName : elementNames) {
                if (Log.isDebugEnabled(Geonet.CSW))
                    Log.debug(Geonet.CSW, "ElementName: " + elementName);
            }
        } else {
            if (Log.isDebugEnabled(Geonet.CSW))
                Log.debug(Geonet.CSW, "No ElementNames found in request");
        }
        // TODO end if(isDebugEnabled)
        return elementNames;
    }

    /**
     * Retrieves the values of attribute typeNames. If typeNames contains csw:BriefRecord or csw:SummaryRecord, an
     * exception is thrown.
     *
     * @param query the query
     * @return list of typenames, or null if not found
     * @throws InvalidParameterValueEx if a typename is illegal
     */
    //    private Set<String> getTypeNames(Element query) throws InvalidParameterValueEx {
    //        Set<String> typeNames = null;
    //        String typeNames$ = query.getAttributeValue("typeNames");
    //        if(typeNames$ != null) {
    //            Scanner commaSeparatedScanner = new Scanner(typeNames$).useDelimiter(",");
    //            while(commaSeparatedScanner.hasNext()) {
    //                String typeName = commaSeparatedScanner.next().trim();
    //                // These two are explicitly not allowed as search targets in CSW 2.0.2, so we throw an exception if the
    //                // client asks for them
    //                if (typeName.equals("csw:BriefRecord") || typeName.equals("csw:SummaryRecord")) {
    //                    throw new InvalidParameterValueEx("typeName", typeName);
    //                }
    //                if(typeNames == null) {
    //                    typeNames = new HashSet<String>();
    //                }
    //                typeNames.add(typeName);
    //            }
    //        }
    //        // TODO in if(isDebugEnabled) condition. Jeeves LOG doesn't provide that useful function though.
    //        if(typeNames != null) {
    //            for(String typeName : typeNames) {
    //                if(Log.isDebugEnabled(Geonet.CSW))
    //                    Log.debug(Geonet.CSW, "TypeName: " + typeName);
    //            }
    //        }
    //        else {
    //            if(Log.isDebugEnabled(Geonet.CSW))
    //                Log.debug(Geonet.CSW, "No TypeNames found in request");
    //        }
    //        // TODO end if(isDebugEnabled)
    //        return typeNames;
    //    }

}