/*
* This file is part of rasdaman community.
*
* Rasdaman community 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 3 of the License, or
* (at your option) any later version.
*
* Rasdaman community 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 rasdaman community. If not, see .
*
* Copyright 2003, 2004, 2005, 2006, 2007, 2008, 2009 Peter Baumann /
rasdaman GmbH.
*
* For more information please see
* or contact Peter Baumann via .
*/
package petascope.wcst.transaction;
//~--- non-JDK imports --------------------------------------------------------
import java.sql.SQLException;
import net.opengis.ows.v_1_0_0.BoundingBoxType;
import net.opengis.wcs.ows.v_1_1_0.InterpolationMethodType;
import net.opengis.wcs.v_1_1_0.CoverageDescriptionType;
import net.opengis.wcs.v_1_1_0.CoverageDescriptions;
import net.opengis.wcs.v_1_1_0.CoverageSummaryType;
import net.opengis.wcs.v_1_1_0.FieldType;
import net.opengis.wcs.v_1_1_0.RangeType;
import org.apache.commons.io.IOUtils;
import petascope.wcps.server.core.SDU;
//~--- JDK imports ------------------------------------------------------------
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import javax.imageio.ImageIO;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.Unmarshaller;
import net.opengis.wcs.ows.v_1_1_0.InterpolationMethods;
import org.odmg.ODMGException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import petascope.wcps.server.core.DbMetadataSource;
import petascope.wcps.server.core.Metadata;
import petascope.wcs.server.exceptions.WCSException;
import petascope.ConfigManager;
import petascope.wcps.server.core.CellDomainElement;
import petascope.wcps.server.core.DomainElement;
import petascope.wcps.server.core.InterpolationMethod;
import petascope.wcps.server.exceptions.InvalidMetadataException;
import petascope.wcps.server.exceptions.InvalidWcpsRequestException;
import petascope.wcps.server.core.RangeElement;
import petascope.wcps.server.exceptions.ResourceException;
import petascope.wcs.server.exceptions.ActionNotSupportedException;
import petascope.wcs.server.exceptions.InputOutputException;
import petascope.wcs.server.exceptions.InternalComponentException;
import petascope.wcs.server.exceptions.InternalSqlException;
import petascope.wcs.server.exceptions.InvalidParameterValueException;
import petascope.wcs.server.exceptions.InvalidRequestException;
import petascope.wcs.server.exceptions.MissingParameterValueException;
import petascope.wcs.server.exceptions.MultiBandImagesNotSupportedException;
import petascope.wcs.server.exceptions.NoApplicableCodeException;
import petascope.wcs.server.exceptions.NodeParsingNotImplementedException;
import petascope.wcs.server.exceptions.XmlNotValidException;
import petascope.wcs.server.exceptions.XmlStructuresException;
import wcst.transaction.schema.CodeType;
import wcst.transaction.schema.CoverageType;
import wcst.transaction.schema.KeywordsType;
import wcst.transaction.schema.LanguageStringType;
import wcst.transaction.schema.ManifestType;
import wcst.transaction.schema.ReferenceType;
import wcst.transaction.schema.TransactionResponseType;
import wcst.transaction.schema.TransactionType;
import petascope.wcst.transaction.tools.RasdamanUtils;
/**
* This class takes a WCS-T Transaction XML request and executes the request,
* building the corresponding XML respose. Use this class for synchronous processing.
*
* @author Andrei Aiordachioaie
*/
public class executeTransaction {
private static Logger LOG = LoggerFactory.getLogger(executeTransaction.class);
// private static boolean printLog = true;
private boolean finished;
private TransactionType input;
protected TransactionResponseType output;
private RasdamanUtils rasUtils;
private String requestId;
private DbMetadataSource metaDb;
private HashSet newCoverages;
/**
* Default constructor. Initialize internal variables.
* @param tr Transaction object, a WCS-T request
* @param metadataDbPath Path to the "dbparams.properties" file
*/
public executeTransaction(TransactionType tr, DbMetadataSource source) throws WCSException {
input = tr;
output = new TransactionResponseType();
finished = false;
metaDb = source;
newCoverages = new HashSet();
String server = ConfigManager.RASDAMAN_URL;
String db = ConfigManager.RASDAMAN_DATABASE;
rasUtils = new RasdamanUtils(server, db);
rasUtils.init();
// In case no-one will ever call this method and we need it
generateRequestId();
}
/**
* Generate a new Request ID string, and return it. If the transaction request
* does not include a request ID, the last generated string will be used.
* @return String Request ID
*/
public String generateRequestId() {
requestId = String.valueOf(UUID.randomUUID());
return requestId;
}
/**
* Main method of this class: Computes the response to the TransactionResponse
* request given to the constructor. If needed, it also calls process()
* @return a TransactionResponse object.
* @throws WCSException
*/
public TransactionResponseType get() throws WCSException {
try {
if (finished == false) {
metaDb.ensureConnection();
process();
}
} catch (SQLException e) {
throw new InternalSqlException("Could not ensure connection to database is valid", e);
}
if (finished == false) {
throw new NoApplicableCodeException("Could not execute the Transaction request! "
+ "Please see the other errors...");
}
return output;
}
/**
* Computes the response to the Transaction request given to the constructor.
*/
public void process() throws WCSException {
if (!input.getService().equalsIgnoreCase("WCS")) {
throw new InvalidParameterValueException("Service. Explanation: Service must be \"WCS\" !");
}
if (!input.getVersion().equalsIgnoreCase("1.1")) {
throw new InvalidParameterValueException("Service. Explanation: Service Version must be \"1.1\" !");
}
// Set the output request ID
String reqID = input.getRequestId();
if (reqID == null) {
reqID = "Request_" + requestId;
generateRequestId();
}
output.setRequestId(reqID);
// All actions succeed or fail as one group.
try {
ManifestType covs = input.getInputCoverages();
List l = covs.getReferenceGroup();
for (int i = 0; i < l.size(); i++) {
// This object is the XML element "InputCoverages"
Object obj = ((JAXBElement) l.get(i)).getValue();
CoverageType cov = (CoverageType) obj;
// Each action adds something to the output XML document
processInputCoverageNode(cov);
}
finished = true;
/* Commit rasdaman changes */
try {
LOG.debug("Commit rasdaman changes ...");
rasUtils.commitAndClose();
LOG.debug("Rasdaman coverages saved successfully !");
} catch (ODMGException e) {
throw new InternalComponentException("Could not commit Rasdaman changes !", e);
}
/* Commit metadata changes */
try {
LOG.debug("Commit metadata changes ...");
metaDb.commitAndClose();
LOG.debug("Metadata has been saved !");
} catch (SQLException e) {
throw new InternalSqlException("Could not commit metadata changes", e);
}
} catch (WCSException e) {
// One action failed, therefore all actions have failed
/* Abort metadata changes */
finished = false;
try {
LOG.debug("Rolling back metadata database changes ...");
metaDb.abortAndClose();
LOG.debug("Metadata rollback completed!");
} catch (SQLException ex) {
LOG.error("Could not rollback metadata changes.");
e.appendErrorDetail(" Could not rollback metadata changes!");
}
/* Abort rasdaman changes */
try {
LOG.debug("Aborting rasdaman changes ...");
rasUtils.abortAndClose();
LOG.debug("Rasdaman changes aborted !");
} catch (ODMGException ex) {
LOG.error("Could not abort rasdaman changes.");
e.appendErrorDetail(" Could not rollback rasdaman changes!");
}
throw e;
}
}
/**
* Delete a coverage from the Rasdaman server.
* @param identifier Name of coverage
* @throws Exception
*/
private void deleteCoverageFromRasdaman(String identifier) throws Exception {
try {
LOG.trace("Deleting coverage from Rasdaman ...");
rasUtils.deleteCollection(identifier);
LOG.trace("Rasdaman Collection '" + identifier + "' is now deleted !");
} catch (ODMGException e) {
LOG.error("Failed to delete rasdaman collection " + identifier);
throw new InternalComponentException("Failed to delete collection from Rasdaman !", e);
}
}
/**
* Insert pixel data for a coverage into RasDaMan DB system
*
* @param identifier Identifier of the coverage
* @param href The location of the pixels for the new image
*/
private void insertImageIntoRasdaman(String identifier, BufferedImage img)
throws WCSException {
LOG.trace("Inserting image into Rasdaman raster server...");
try {
rasUtils.insertGrayImageAsArray(identifier, img);
LOG.debug("Inserted image into Rasdaman !");
} catch (ODMGException e) {
LOG.error("Could not insert image into Rasdaman !");
throw new InternalComponentException("Could not insert image into Rasdaman.", e);
}
}
/** Load a BufferedImage from a ReferenceType object.
*
* @param pixels Reference object
* @return available image
* @throws WCSException
*/
private BufferedImage loadPixelsReference(ReferenceType pixels) throws WCSException {
URL url = null;
BufferedImage img = null;
try {
url = new URL(pixels.getHref());
} catch (MalformedURLException e) {
LOG.error("URL " + url.toString() + " is not valid.");
throw new InvalidParameterValueException("Reference pixels. "
+ "Explanation: URL " + url.toString() + " is not valid.");
}
try {
img = ImageIO.read(url);
if (img == null) {
throw new IOException("Empty stream while reading image.");
}
} catch (IOException e) {
LOG.error(e.getMessage());
throw new InputOutputException("Reference Pixels. Explanation: "
+ "Could not read image from URL '" + url, e);
}
return img;
}
/**
* Load a Coverage Description XML object from a Reference
*
* @param identifier Name of coverage
* @param desc Reference to a CoverageDescriptions xml
* @return coverage description
* @throws WCSException
*/
private CoverageDescriptionType loadDescriptionReference(String identifier,
ReferenceType desc) throws WCSException {
URL url = null;
String xmlString = null;
CoverageDescriptions descs = null;
CoverageDescriptionType desc0 = null;
// Load the URL
try {
url = new URL(desc.getHref());
} catch (MalformedURLException e) {
LOG.error("URL " + url.toString() + " is not valid !");
throw new InvalidParameterValueException("Reference pixels. Explanation: "
+ "URL " + url.toString() + " is not valid.");
}
// Read the contents of the URL
try {
URLConnection conn = url.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
xmlString = IOUtils.toString(in);
} catch (IOException ex) {
throw new InputOutputException("Description Reference. Explanation: error loading the "
+ "coverage description from URL " + url.toString(), ex);
}
// Unmarshall the XML string into a Java Object
try {
JAXBContext jaxbCtx = JAXBContext.newInstance(CoverageDescriptions.class.getPackage().getName());
Unmarshaller unmarshaller = jaxbCtx.createUnmarshaller();
Object obj = unmarshaller.unmarshal(new StringReader(xmlString));
if (obj instanceof JAXBElement) {
descs = (CoverageDescriptions) ((JAXBElement) obj).getValue();
} else if (obj instanceof CoverageDescriptions) {
descs = (CoverageDescriptions) obj;
} else {
LOG.error("Coverage description metadata is not a valid xml document.");
throw new XmlNotValidException("Coverage "
+ "description metadata is not a valid xml document.");
}
} catch (javax.xml.bind.JAXBException ex) {
throw new XmlStructuresException("Could not marshall/unmarshall XML structures.", ex);
}
// Filter by coverage name
desc0 = null;
Iterator i = descs.getCoverageDescription().iterator();
while (i.hasNext()) {
CoverageDescriptionType d = i.next();
if (d.getIdentifier().equals(identifier)) {
desc0 = d;
break;
}
}
if (desc0 == null) {
throw new InvalidRequestException("Could not find a CoverageDescription for coverage: " + identifier);
}
return desc0;
}
private CoverageSummaryType loadSummaryReference(ReferenceType pixels) throws WCSException {
URL url = null;
String xmlString = null;
CoverageSummaryType xml = null;
try {
url = new URL(pixels.getHref());
} catch (MalformedURLException e) {
throw new InvalidParameterValueException("Reference summary. Explanation: URL is not valid.", e);
}
// Read the contents of the URL
try {
URLConnection conn = url.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
xmlString = IOUtils.toString(in);
} catch (IOException ex) {
throw new InputOutputException("Summary Reference. Explanation: "
+ "Error loading the " + "coverage summary from URL " + url.toString(), ex);
}
// Unmarshall the XML string into a Java Object
try {
JAXBContext jaxbCtx = JAXBContext.newInstance(CoverageSummaryType.class.getPackage().getName());
Unmarshaller unmarshaller = jaxbCtx.createUnmarshaller();
Object obj = unmarshaller.unmarshal(new StringReader(xmlString));
if (obj instanceof JAXBElement) {
xml = (CoverageSummaryType) ((JAXBElement) obj).getValue();
} else if (obj instanceof CoverageSummaryType) {
xml = (CoverageSummaryType) obj;
} else {
throw new XmlNotValidException("Coverage Summary metadata is not a valid xml document.");
}
} catch (javax.xml.bind.JAXBException ex) {
throw new XmlStructuresException("Could not marshall/unmarshall XML structures.", ex);
}
return xml;
}
/**
* Updates the coverage metadata: textual descriptions. The title and the abstract
* could be specified in multiple languages, but we only store english.
*
* @param meta Metadata object to be modified
* @param summary summary object, that contains title, abstract and coverage keywords
* @return modified metadata object
*/
private Metadata updateMetadataWithSummary(Metadata meta,
CoverageSummaryType summary) throws WCSException {
LOG.debug("Updating metadata with values from Coverage Summary...");
String title = null, abstr = null, keywords = null;
title = summary.getTitle();
abstr = summary.getAbstract();
List keywordL = summary.getKeywords();
List kList = new ArrayList();
Iterator i = keywordL.iterator();
while (i.hasNext()) {
KeywordsType keywordInMultipleLangs = (KeywordsType) i.next();
String keyword = filterAcceptedLanguage(keywordInMultipleLangs.getKeyword());
kList.add(keyword);
}
keywords = SDU.string2str(kList);
meta.setTitle(title);
meta.setKeywords(keywords);
meta.setAbstract(abstr);
return meta.clone();
}
/**
* Retrieve only the string in the accepted language from a multiple-language list
*
* @param list List of strings in several languages
* @return String in the accepted language, or null if none is found
*/
private String filterAcceptedLanguage(List list) {
String result = null;
Iterator i = list.iterator();
while (i.hasNext()) {
LanguageStringType a = (LanguageStringType) i.next();
if (a.getLang().equals(ConfigManager.WCST_LANGUAGE)) {
result = a.getValue();
}
}
return result;
}
/**
* Partially updates a Rasdaman coverage with pixels from pixHref. Info about
* the bounding box is available from descHref.
*
* @param identifier ID of the coverage to be updates
* @param pixHref URL for the pixels data
* @param descHref URL for the metadata
*/
private void insertSomePixelsIntoRasdaman(String identifier, String pixHref, String descHref) {
// TODO: Implement !
LOG.error("Partial update is not yet implemented");
throw new UnsupportedOperationException("Partial Rasdaman update is not yet implemented.");
}
/**
* Inserts default metadata values for the given coverage.
*
* @param identifier ID of the coverage
* @param img The image, fetched from external reference
* @throws WCSException on error
*/
private Metadata createNewCoverageMetadata(String identifier, BufferedImage img)
throws InvalidMetadataException {
Metadata m = null;
LOG.debug("Creating metadata with default values...");
// TODO: When we accept multi-band images, update nullDefault
String nullDefault = "0";
// Cell domains
BigInteger lowX = new BigInteger("0");
BigInteger highX = new BigInteger(String.valueOf(img.getHeight() - 1));
BigInteger lowY = new BigInteger("0");
BigInteger highY = new BigInteger(String.valueOf(img.getWidth() - 1));
CellDomainElement cellX = new CellDomainElement(lowX, highX);
CellDomainElement cellY = new CellDomainElement(lowY, highY);
List cellList = new ArrayList(2);
cellList.add(cellX);
cellList.add(cellY);
// Domains
Set crsSet = new HashSet(1);
crsSet.add(DomainElement.IMAGE_CRS);
String str1 = null, str2 = null;
/* Since we currently do not use the Domain sizes, we can set them to 0 and 1 */
DomainElement domX = new DomainElement("x", "x", 0.0, 1.0, str1, str2, crsSet, metaDb.getAxisNames());
DomainElement domY = new DomainElement("y", "y", 0.0, 1.0, str1, str2, crsSet, metaDb.getAxisNames());
List domList = new ArrayList(2);
domList.add(domX);
domList.add(domY);
// Ranges
/* TODO: When multiple-field images are supported, update ranges */
RangeElement range = new RangeElement("intensity", ConfigManager.WCST_DEFAULT_DATATYPE);
List rList = new ArrayList(1);
rList.add(range);
// Interpolation methods: only the default
String interpMeth = ConfigManager.WCST_DEFAULT_INTERPOLATION;
String nullRes = ConfigManager.WCST_DEFAULT_NULL_RESISTANCE;
InterpolationMethod interp = new InterpolationMethod(interpMeth, nullRes);
Set interpList = new HashSet(1);
interpList.add(interp);
// Null sets
/* TODO: update for multi-band images */
String nullVal = "0";
Set nullSet = new HashSet(1);
nullSet.add(nullVal);
// Descriptions
String abstr = null;
String title = "Coverage " + identifier;
String keywords = null;
m =
new Metadata(cellList, rList, nullSet, nullDefault, interpList,
interp, identifier, domList, null, title, abstr, keywords);
LOG.debug("Done creating default metadata");
return m;
}
/**
* Processes one node from the input XML
*
* @param elem the JAXB node equivalent to the node
*/
private void processInputCoverageNode(CoverageType elem) throws WCSException {
if (elem.getAction() == null) {
throw new InvalidParameterValueException("Action. Explanation: "
+ "Every node must contain an child node.");
}
String action = elem.getAction().getValue();
String identifier = null;
List references = null;
if (elem.getIdentifier() == null) {
throw new InvalidParameterException("Identifier");
}
identifier = elem.getIdentifier().getValue();
references = elem.getAbstractReferenceBase();
if (action.equalsIgnoreCase("Add")) {
actionAddCoverage(identifier, references);
} else if (action.equalsIgnoreCase("UpdateMetadata")) {
actionUpdateMetadata(identifier, references);
} else if (action.equalsIgnoreCase("Delete")) {
actionDeleteCoverage(identifier, references);
} else if (action.equalsIgnoreCase("UpdateAll")) {
actionUpdateAll(identifier, references);
} else if (action.equalsIgnoreCase("UpdateDataPart")) {
throw new ActionNotSupportedException("Action \"UpdateDataPart\" is not supported yet.");
/* TODO: UpdateDataPart is not yet functional. The Rasdaman server
* returns with an unexpected internal error (code: 10000) when
* a partial update query is sent. */
// actionUpdateDataPart(identifier, references);
}
}
/**
* Performs the action "UpdateAll", as part of the Transaction operation
*
* @param identifier Name of coverage to update
* @param references List of references with data for update
*/
private void actionUpdateAll(String identifier, List references) throws WCSException {
LOG.trace("Executing action Update All ...");
actionUpdateDataPart(identifier, references);
actionUpdateMetadata(identifier, references);
LOG.trace("Finished action Update All!");
}
/**
* Updates the Metadata DB with the information contained in the CoverageDescriptions XML object
*
* @param identifier ID of the coverage
* @param desc object that contains the coverage description.
*/
private Metadata updateMetadataWithDescription(Metadata meta, CoverageDescriptionType desc) throws WCSException {
LOG.debug("Updating metadata with values from CoverageDescription...");
/* (B) Table ps_descriptions: Update coverage title, abstract, keywords */
String title = desc.getTitle();
String abstr = desc.getAbstract();
String keywords = desc.getKeywords().toString();
meta.setAbstract(abstr);
meta.setKeywords(keywords);
meta.setTitle(title);
/* (C) Table ps_range: Update field name, types, and interpolation methods */
if (desc.getRange() != null) {
Set interpSet = new HashSet();
RangeType range = desc.getRange();
List fields = range.getField();
try {
LOG.debug("Updating range information...");
Iterator i = fields.iterator();
ArrayList rangeList = new ArrayList();
while (i.hasNext()) {
FieldType field = i.next();
String name = field.getIdentifier();
String datatype = field.getDefinition().getDataType().getValue();
RangeElement fieldRange = new RangeElement(name, datatype);
rangeList.add(fieldRange);
InterpolationMethods methods = field.getInterpolationMethods();
String interpType = methods.getDefaultMethod().getValue();
String nullResist = methods.getDefaultMethod().getNullResistance();
InterpolationMethod interp = new InterpolationMethod(interpType, nullResist);
interpSet.add(interp);
Iterator it = methods.getOtherMethod().iterator();
while (it.hasNext()) {
InterpolationMethodType imt = it.next();
String type = imt.getValue();
String resis = imt.getNullResistance();
interp = new InterpolationMethod(type, resis);
interpSet.add(interp);
}
}
meta.setRange(rangeList);
} catch (InvalidMetadataException e) {
throw new InternalComponentException("Invalid metadata.", e);
}
meta.setInterpolationSet(interpSet);
}
/* (D) Table ps_coverage: Update default interpolation method and null resistance */
/*
* We store interpolation methods at coverage level, not field level.
* So we only look at the interpolation method list of the first field,
* and use it on the whole coverage
*/
if (desc.isSetRange()) {
try {
LOG.debug("Updating default interpolation method...");
InterpolationMethodType def1 = desc.getRange().getField().get(0).getInterpolationMethods().getDefaultMethod();
String method = def1.getValue();
String resist = def1.getNullResistance();
InterpolationMethod meth = new InterpolationMethod(method, resist);
meta.setDefaultInterpolation(meth);
} catch (InvalidMetadataException e) {
throw new InternalComponentException("Invalid metadata.", e);
}
}
/* (E) Table ps_celldomain: Update cell domain of the coverage. */
/* NOTE: Only works for 2-D (x/y) or 3-D (x/y/t) coverages */
if (desc.isSetDomain()) {
LOG.debug("Updating spatial bounding box of coverage ...");
try {
List> list =
desc.getDomain().getSpatialDomain().getBoundingBox();
if (list.size() == 1) {
BoundingBoxType bbox = (BoundingBoxType) list.get(0).getValue();
if (bbox.getCrs() == null || bbox.getCrs().equals(DomainElement.IMAGE_CRS)) {
meta = updateImageCrsBoundingBox(meta, bbox);
} else {
throw new InvalidParameterValueException("crs. Explanation: Unknown CRS " + bbox.getCrs());
}
} else {
Iterator i = list.iterator();
while (i.hasNext()) {
BoundingBoxType bbox = (BoundingBoxType) i.next();
if (bbox.getCrs().equals(DomainElement.IMAGE_CRS)) {
meta = updateImageCrsBoundingBox(meta, bbox);
}
// TODO: Implement WGS84 update
// if (bbox.getCrs().equals(DomainElement.WGS84_CRS))
// updateWgs84CrsBoundingBox(meta, bbox);
}
}
} catch (InvalidMetadataException e) {
throw new InternalComponentException("Invalid metadata.", e);
}
}
if (desc.getDomain().isSetTemporalDomain()) {
LOG.debug("Updating temporal bounding box of coverage ...");
/*
try
{
List