/* See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* Esri Inc. licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.esri.gpt.server.csw.provider.local;
import com.esri.gpt.catalog.discovery.DcElement;
import com.esri.gpt.catalog.discovery.DiscoveredRecord;
import com.esri.gpt.catalog.discovery.DiscoveredRecords;
import com.esri.gpt.catalog.discovery.DiscoveryQuery;
import com.esri.gpt.catalog.discovery.DiscoveryResult;
import com.esri.gpt.catalog.discovery.PropertyMeaningType;
import com.esri.gpt.catalog.discovery.PropertyMeaning;
import com.esri.gpt.catalog.discovery.PropertyValueType;
import com.esri.gpt.catalog.discovery.Returnable;
import com.esri.gpt.framework.collection.StringSet;
import com.esri.gpt.framework.geometry.Envelope;
import com.esri.gpt.framework.util.Val;
import com.esri.gpt.framework.xml.DomUtil;
import com.esri.gpt.framework.xml.XmlIoUtil;
import com.esri.gpt.server.csw.provider.components.CswConstants;
import com.esri.gpt.server.csw.provider.components.CswNamespaces;
import com.esri.gpt.server.csw.provider.components.IOriginalXmlProvider;
import com.esri.gpt.server.csw.provider.components.IResponseGenerator;
import com.esri.gpt.server.csw.provider.components.OperationContext;
import com.esri.gpt.server.csw.provider.components.OperationResponse;
import com.esri.gpt.server.csw.provider.components.QueryOptions;
import com.esri.gpt.server.csw.provider.components.ServiceProperties;
import java.sql.Timestamp;
import java.util.logging.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* Generates a CSW query response.
* <p>
* Applies to the GetRecordById and GetRecords operation response.
*/
public class QueryResponse extends DiscoveryAdapter implements IResponseGenerator {
/** class variables ========================================================= */
/** The Logger. */
private static Logger LOGGER = Logger.getLogger(QueryResponse.class.getName());
/** constructors ============================================================ */
/** Default constructor */
public QueryResponse(OperationContext context) {
super(context);
}
/** methods ================================================================= */
/**
* Creates and appends elements associated with a returnable property to a
* record element.
* @param context the operation context
* @param record the parent element that will hold the fields (a Record)
* @param returnable the returnable property
*/
protected void appendDiscoveredField(OperationContext context,
Element record,
Returnable returnable) {
// initialize
ServiceProperties svcProps = context.getServiceProperties();
String httpContextPath = Val.chkStr(svcProps.getHttpContextPath());
String cswBaseUrl = Val.chkStr(svcProps.getCswBaseURL());
OperationResponse opResponse = context.getOperationResponse();
Document responseDom = opResponse.getResponseDom();
PropertyMeaning meaning = returnable.getMeaning();
PropertyMeaningType meaningType = meaning.getMeaningType();
Object[] values = returnable.getValues();
DcElement dcElement = meaning.getDcElement();
if ((dcElement == null) || dcElement.getElementName().startsWith("!")) {
return;
}
// TODO create an empty element if the values are null?
// return if the values are null
if (values == null) {
//Element elField = dom.createElement(returnable.getClientName());
//elField.appendChild(dom.createTextNode(""));
//record.appendChild(elField);
return;
}
// add an element for each value found
for (Object oValue: values) {
if (oValue != null) {
if (meaning.getValueType().equals(PropertyValueType.GEOMETRY)) {
if (oValue instanceof Envelope) {
// TODO include multiple envelope types in the response
Envelope env = (Envelope)oValue;
String sLower = env.getMinX()+" "+env.getMinY();
String sUpper = env.getMaxX()+" "+env.getMaxY();
Element elField = responseDom.createElement("ows:WGS84BoundingBox");
Element elLower = responseDom.createElement("ows:LowerCorner");
Element elUpper = responseDom.createElement("ows:UpperCorner");
elLower.appendChild(responseDom.createTextNode(sLower));
elUpper.appendChild(responseDom.createTextNode(sUpper));
elField.appendChild(elLower);
elField.appendChild(elUpper);
record.appendChild(elField);
elField = responseDom.createElement("ows:BoundingBox");
elLower = responseDom.createElement("ows:LowerCorner");
elUpper = responseDom.createElement("ows:UpperCorner");
elLower.appendChild(responseDom.createTextNode(sLower));
elUpper.appendChild(responseDom.createTextNode(sUpper));
elField.appendChild(elLower);
elField.appendChild(elUpper);
record.appendChild(elField);
}
} else {
String sValue = oValue.toString();
if (oValue instanceof Timestamp) {
if (meaningType.equals(PropertyMeaningType.DATEMODIFIED)) {
sValue = opResponse.toIso8601((Timestamp)oValue);
} else {
sValue = opResponse.toIso8601Date((Timestamp)oValue);
}
}
if (meaningType.equals(PropertyMeaningType.XMLURL)) {
if ((sValue != null) && sValue.startsWith("?getxml=")) {
sValue = cswBaseUrl+sValue;
}
} else if (meaningType.equals(PropertyMeaningType.THUMBNAILURL)) {
if ((sValue != null) && sValue.startsWith("/thumbnail?uuid")) {
sValue = httpContextPath+sValue;
}
}
if ((sValue != null) && (dcElement != null) && (dcElement.getElementName().length() > 0)) {
String elName = dcElement.getElementName().replaceAll("~","");
Element elField = responseDom.createElement(elName);
elField.appendChild(responseDom.createTextNode(sValue));
if (dcElement.getScheme().length() > 0) {
elField.setAttribute("scheme",dcElement.getScheme());
// don't return unknown content types
if (sValue.equalsIgnoreCase("unknown")) {
if (dcElement.getScheme().toLowerCase().endsWith("contenttype")) {
elField = null;
}
}
}
if (elField != null) {
record.appendChild(elField);
}
}
}
}
}
}
/**
* Creates and appends the discovered record elements to the XML document.
* <br/>Applies to csw:GetRecordByIdResponse and csw:GetRecordsResponse.
* @param context the operation context
* @param parent the parent element that will hold the records
*/
protected void appendDiscoveredRecords(OperationContext context, Element parent)
throws Exception {
// determine the record element's namespace URI, namespace prefix,
// local name and qualified name
QueryOptions qOptions = context.getRequestOptions().getQueryOptions();
DiscoveryQuery query = this.getDiscoveryContext().getDiscoveryQuery();
boolean isDublinCore = qOptions.isDublinCoreResponse();
String elementSetType = Val.chkStr(qOptions.getElementSetType());
String outputSchema = Val.chkStr(qOptions.getOutputSchema());
StringSet elementSetTypeNames = qOptions.getElementSetTypeNames();
boolean isBrief = elementSetType.equalsIgnoreCase(CswConstants.ElementSetType_Brief);
boolean isSummary = elementSetType.equalsIgnoreCase(CswConstants.ElementSetType_Summary);
boolean isFull = elementSetType.equalsIgnoreCase(CswConstants.ElementSetType_Full);
// Dublin Core based responses
if (isDublinCore) {
OperationResponse opResponse = context.getOperationResponse();
Document responseDom = opResponse.getResponseDom();
String recNamespacePfx = "csw";
String recNamespaceUri = CswNamespaces.URI_CSW;
String recLocalName = "Record";
StringSet elementNames = qOptions.getElementNames();
boolean hasElementNames = (elementNames != null) && (elementNames.size() > 0);
if (!hasElementNames) {
if (isBrief) {
recLocalName = "BriefRecord";
} else if (isSummary) {
recLocalName = "SummaryRecord";
}
}
String recQName = recNamespacePfx+":"+recLocalName;
// append each record
DiscoveredRecords records = query.getResult().getRecords();
for (DiscoveredRecord record: records) {
Element elRecord = responseDom.createElementNS(recNamespaceUri,recQName);
for (Returnable returnable: record.getFields()) {
this.appendDiscoveredField(context,elRecord,returnable);
}
parent.appendChild(elRecord);
}
// non Dublin Core based responses
} else {
IOriginalXmlProvider oxp = context.getProviderFactory().makeOriginalXmlProvider(context);
DiscoveredRecords records = query.getResult().getRecords();
for (DiscoveredRecord record: records) {
String responseXml = Val.chkStr(record.getResponseXml());
if (responseXml.length() > 0) {
Document dom = DomUtil.makeDomFromString(responseXml,true);
NodeList nl = dom.getChildNodes();
for (int i=0; i<nl.getLength(); i++) {
if (nl.item(i).getNodeType() == Node.ELEMENT_NODE){
Node ndXml = nl.item(i);
Node ndImported = parent.getOwnerDocument().importNode(ndXml,true);
parent.appendChild(ndImported);
break;
}
}
} else {
//TODO throw exception here?
}
}
}
}
/**
* Creates and appends the csw:SearchResults element to the XML document.
* <br/>Applies to csw:GetRecordsResponse.
* <br/>Discovered records are also created and appended.
* @param parent the parent element that will hold the results (typically the root)
*/
protected void appendSearchResultsElement(OperationContext context, Element parent)
throws Exception{
// determine records counts
QueryOptions qOptions = context.getRequestOptions().getQueryOptions();
DiscoveryQuery query = this.getDiscoveryContext().getDiscoveryQuery();
OperationResponse opResponse = context.getOperationResponse();
Document responseDom = opResponse.getResponseDom();
DiscoveryResult result = query.getResult();
int numberOfRecordsMatched = result.getNumberOfHits();
int numberOfRecordsReturned = 0;
int nextRecord = 0;
String rtname = qOptions.getResultType();
boolean isHits = rtname.equalsIgnoreCase(CswConstants.ResultType_Hits);
if (isHits || (query.getFilter().getMaxRecords() <= 0)) {
if (numberOfRecordsMatched > 0) {
nextRecord = 1;
}
} else {
numberOfRecordsReturned = result.getRecords().size();
if (numberOfRecordsReturned > 0) {
if (numberOfRecordsReturned < numberOfRecordsMatched) {
nextRecord = query.getFilter().getStartRecord() + numberOfRecordsReturned;
if (nextRecord > numberOfRecordsMatched) nextRecord = 0;
}
}
}
// the following attributes are not currently populated:
// resultSetId expires
// create and add the status element
String sTimestamp = opResponse.toIso8601(new Timestamp(System.currentTimeMillis()));
Element elStatus = responseDom.createElementNS(CswNamespaces.URI_CSW,"csw:SearchStatus");
elStatus.setAttribute("timestamp",sTimestamp);
parent.appendChild(elStatus);
// create and add the results element
Element elResults = responseDom.createElementNS(CswNamespaces.URI_CSW,"csw:SearchResults");
elResults.setAttribute("numberOfRecordsMatched",""+numberOfRecordsMatched);
elResults.setAttribute("numberOfRecordsReturned",""+numberOfRecordsReturned);
if (nextRecord >= 0) {
elResults.setAttribute("nextRecord",""+nextRecord);
}
StringSet elementNames = qOptions.getElementNames();
boolean hasElementNames = (elementNames != null) && (elementNames.size() > 0);
if (!hasElementNames) {
String type = qOptions.getElementSetType();
elResults.setAttribute("elementSet",type);
}
if (qOptions.isDublinCoreResponse()) {
elResults.setAttribute("recordSchema",CswNamespaces.URI_CSW);
} else {
elResults.setAttribute("recordSchema",qOptions.getOutputSchema());
}
parent.appendChild(elResults);
// append the discovered records
if (!isHits) {
this.appendDiscoveredRecords(context,elResults);
}
}
/**
* Generates the response.
* @param context the operation context
* @throws Exception if a processing exception occurs
*/
public void generateResponse(OperationContext context) throws Exception {
String opName = Val.chkStr(context.getOperationName());
if (opName.equalsIgnoreCase(CswConstants.Operation_GetRecordById)) {
LOGGER.finer("Generating GetRecordByIdResponse...");
OperationResponse opResponse = context.getOperationResponse();
Element root = this.newResponseDom(context,"GetRecordByIdResponse");
this.appendDiscoveredRecords(context,root);
opResponse.setResponseXml(XmlIoUtil.domToString(opResponse.getResponseDom()));
} else if (opName.equalsIgnoreCase(CswConstants.Operation_GetRecords)) {
LOGGER.finer("Generating GetRecordsResponse...");
OperationResponse opResponse = context.getOperationResponse();
Element root = this.newResponseDom(context,"GetRecordsResponse");
this.appendSearchResultsElement(context,root);
opResponse.setResponseXml(XmlIoUtil.domToString(opResponse.getResponseDom()));
}
}
/**
* Creates a new XML document for response construction.
* @param rootName the name of the root element
* @return the root element
* @throws Exception if a processing exception occurs
*/
public Element newResponseDom(OperationContext context,String rootName) throws Exception {
QueryOptions qOptions = context.getRequestOptions().getQueryOptions();
if (qOptions.isDublinCoreResponse()) {
return context.getOperationResponse().newResponseDom(rootName);
} else {
Document dom = DomUtil.newDocument();
if (!rootName.startsWith("csw:")) {
rootName = "csw:"+rootName;
}
Element root = dom.createElementNS(CswNamespaces.URI_CSW,rootName);
root.setAttribute("xmlns:csw",CswNamespaces.URI_CSW);
dom.appendChild(root);
context.getOperationResponse().setResponseDom(dom);
return root;
}
}
}