/* (c) 2015 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.wps.jdbc; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.nio.charset.Charset; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.parsers.ParserConfigurationException; import org.geoserver.wps.ProcessStatusStore; import org.geoserver.wps.WPSException; import org.geoserver.wps.executor.ExecutionStatus; import org.geoserver.wps.executor.ProcessState; import org.geoserver.wps.xml.WPSConfiguration; import org.geotools.data.DataStore; import org.geotools.data.DataUtilities; import org.geotools.data.DefaultTransaction; import org.geotools.data.Query; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.data.simple.SimpleFeatureStore; import org.geotools.data.transform.Definition; import org.geotools.data.transform.TransformFactory; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.FeatureCollection; import org.geotools.feature.NameImpl; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.filter.text.cql2.CQLException; import org.geotools.filter.text.ecql.ECQL; import org.geotools.util.Converters; import org.geotools.util.logging.Logging; import org.geotools.wps.WPS; import org.geotools.xml.Encoder; import org.geotools.xml.Parser; import org.opengis.feature.Property; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.Name; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory; import org.xml.sax.SAXException; import net.opengis.ows11.Ows11Factory; import net.opengis.wps10.ExecuteType; import net.opengis.wps10.Wps10Factory; /** * A class that stores WPS session statuses in JDBC datastores. * * @author Ian Turton */ public class JDBCStatusStore implements ProcessStatusStore { private static final String STACKTRACESEPERATOR = "/"; static final Logger LOGGER = Logging.getLogger(JDBCStatusStore.class); static final Wps10Factory WPSFACTORY = Wps10Factory.eINSTANCE; static final Ows11Factory OWSFACTORY = Ows11Factory.eINSTANCE; static final String STACK_TRACE = "stackTrace"; static final String EXCEPTION_MESSAGE = "exceptionMessage"; static final String EXCEPTION_CLASS = "exceptionClass"; static final String ASYNC = "async"; static final String USER_NAME = "userName"; static final String TASK = "task"; static final String SIMPLE_PROCESS_NAME = "processName"; static final String PROPERTIES = "properties"; static final String PROGRESS = "progress"; static final String PROCESS_NAME_URI = "processNameURI"; static final String PROCESS_NAME = "processNameImpl"; static final String PHASE = "phase"; static final String NODE_ID = "node"; static final String COMPLETION = "completionTime"; static final String LASTUPDATE = "lastUpdated"; static final String CREATION = "creationTime"; static final String PROCESS_ID = "processId"; static final String STATUS = "status"; static final String EXECUTION_ID = "exceptionId"; private static final String REQUEST = "request"; DataStore statuses; SimpleFeatureType schema; String actualStatusName; List<Definition> mappingDefinitions; public JDBCStatusStore(JDBCStatusStoreLoader loader) { this(loader.getStore()); } public JDBCStatusStore(DataStore store) { if (store == null) { throw new RuntimeException("Attempted to create a JDBCStatusStore with a null datastore"); } SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder(); tb.add(PROCESS_ID, String.class); tb.add(CREATION, Timestamp.class); tb.add(LASTUPDATE, Timestamp.class); tb.add(COMPLETION, Timestamp.class); tb.add(NODE_ID, String.class); tb.add(PHASE, String.class); tb.add(PROCESS_NAME, String.class); tb.add(PROCESS_NAME_URI, String.class); tb.add(PROGRESS, Float.class); tb.add(REQUEST, byte[].class); tb.add(PROPERTIES, String.class); tb.add(SIMPLE_PROCESS_NAME, String.class); tb.add(TASK, String.class); tb.add(USER_NAME, String.class); tb.add(ASYNC, String.class); tb.add(EXCEPTION_CLASS, String.class); tb.add(EXCEPTION_MESSAGE, String.class); tb.add(STACK_TRACE, byte[].class); tb.setName(STATUS); schema = tb.buildFeatureType(); statuses = store; try { SimpleFeatureType storeSchema = lookupStatusSchema(); if (storeSchema == null) { LOGGER.fine("creating new DB table for statuses"); statuses.createSchema(schema); storeSchema = lookupStatusSchema(); } // do we need any mapping? actualStatusName = storeSchema.getTypeName(); mappingDefinitions = buildDefinitions(storeSchema, schema); } catch (IOException e) { throw new WPSException("Failed to setup the underlying store", e); } } private List<Definition> buildDefinitions(SimpleFeatureType actual, SimpleFeatureType expected) { List<Definition> definitions = new ArrayList<>(); boolean mappingRequired = false; if(!actual.getTypeName().equals(expected.getTypeName())) { mappingRequired = true; } List<AttributeDescriptor> expectedDescriptors = expected.getAttributeDescriptors(); List<AttributeDescriptor> actualDescriptors = actual.getAttributeDescriptors(); FilterFactory ff = CommonFactoryFinder.getFilterFactory(); for (int i = 0; i < expected.getAttributeCount(); i++) { AttributeDescriptor expectedDescriptor = expectedDescriptors.get(i); AttributeDescriptor actualDescriptor = actualDescriptors.get(i); String expectedName = expectedDescriptor.getLocalName(); String actualName = actualDescriptor.getLocalName(); if(!expectedName.equals(actualName)) { mappingRequired = true; } Class<?> expectedType = expectedDescriptor.getType().getBinding(); if(!expectedType.isAssignableFrom(actualDescriptor.getType().getBinding())) { mappingRequired = true; } definitions.add(new Definition(expectedName, ff.property(actualName), expectedType)); } if(mappingRequired) { return definitions; } else { return null; } } private SimpleFeatureType lookupStatusSchema() throws IOException { String[] typeNames = statuses.getTypeNames(); for (String typeName : typeNames) { if(typeName.equalsIgnoreCase(STATUS)) { return statuses.getSchema(typeName); } } return null; } private SimpleFeatureStore getStatusFeatureStore() throws IOException { SimpleFeatureSource source = statuses.getFeatureSource(actualStatusName); if(mappingDefinitions != null) { source = TransformFactory.transform(source, new NameImpl(STATUS), mappingDefinitions); } return (SimpleFeatureStore) source; } @Override public void save(ExecutionStatus status) { DefaultTransaction transaction = new DefaultTransaction("create"); boolean committed = false; try { SimpleFeatureStore store = getStatusFeatureStore(); store.setTransaction(transaction); SimpleFeature feature = statusToFeature(status); FeatureCollection<SimpleFeatureType, SimpleFeature> featureCollection = DataUtilities .collection(feature); // if the feature exists delete it Filter filter = ECQL.toFilter(PROCESS_ID + " = '" + status.getExecutionId() + "'"); store.removeFeatures(filter); store.addFeatures(featureCollection); transaction.commit(); committed = true; } catch (Exception e) { throw new WPSException("Failure saving status " + status, e); } finally { closeTransaction(transaction, committed); } } @Override public ExecutionStatus get(String executionId) { LOGGER.fine("getting status " + executionId); try { SimpleFeatureSource source = getStatusFeatureStore(); Filter filter = ECQL.toFilter(PROCESS_ID + " = '" + executionId + "'"); SimpleFeatureCollection features = source.getFeatures(filter); SimpleFeature f = DataUtilities.first(features); ExecutionStatus stat = featureToStatus(f); return stat; } catch (IOException | CQLException e) { throw new WPSException("Failed to get execution status " + executionId, e); } } @Override public ExecutionStatus remove(String executionId) { LOGGER.fine("removing status " + executionId); DefaultTransaction transaction = new DefaultTransaction("create"); boolean committed = false; try { SimpleFeatureStore store = getStatusFeatureStore(); Filter filter = ECQL.toFilter(PROCESS_ID + " = '" + executionId + "'"); store.setTransaction(transaction); SimpleFeatureCollection features = store.getFeatures(filter); SimpleFeature f = DataUtilities.first(features); ExecutionStatus stat = featureToStatus(f); store.removeFeatures(filter); transaction.commit(); committed = true; return stat; } catch (Exception e) { throw new WPSException("Failure to remove status by id: " + executionId, e); } finally { closeTransaction(transaction, committed); } } private void closeTransaction(DefaultTransaction transaction, boolean committed) { if (!committed) { try { transaction.rollback(); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Failure to roll back transaction", e); } } transaction.close(); } @Override public int remove(Filter filter) { LOGGER.fine("removing statuses matching " + filter); int ret = 0; DefaultTransaction transaction = new DefaultTransaction("create"); boolean committed = false; try { SimpleFeatureStore store = getStatusFeatureStore(); SimpleFeatureCollection features = store.getFeatures(filter); ret = features.size(); if (ret == 0) { return ret; } store.setTransaction(transaction); store.removeFeatures(filter); transaction.commit(); committed = true; } catch (Exception e) { throw new WPSException("Failure to remove status by filter: " + filter, e); } finally { closeTransaction(transaction, committed); } return ret; } @Override public List<ExecutionStatus> list(Query query) { LOGGER.fine("listing statuses matching " + query); try { ArrayList<ExecutionStatus> ret = new ArrayList<>(); SimpleFeatureStore source = getStatusFeatureStore(); LOGGER.fine("requesting " + query); SimpleFeatureCollection features = source.getFeatures(query); try(SimpleFeatureIterator itr = features.features()) { while (itr.hasNext()) { SimpleFeature f = itr.next(); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("adding " + f); } ret.add(featureToStatus(f)); } return ret; } } catch (IOException e) { throw new WPSException("Failed to list statuses by query " + query, e); } } protected SimpleFeature statusToFeature(ExecutionStatus status) { SimpleFeatureBuilder builder = new SimpleFeatureBuilder(schema); builder.set(PROCESS_ID, status.getExecutionId()); builder.set(CREATION, status.getCreationTime()); builder.set(LASTUPDATE, status.getLastUpdated()); builder.set(COMPLETION, status.getCompletionTime()); builder.set(NODE_ID, status.getNodeId()); builder.set(PHASE, status.getPhase()); Name processName = status.getProcessName(); builder.set(PROCESS_NAME, processName.getLocalPart()); builder.set(PROCESS_NAME_URI, processName.getNamespaceURI()); builder.set(PROGRESS, status.getProgress()); ExecuteType request = status.getRequest(); if (request != null) { builder.set(REQUEST, serializeRequest(request)); } builder.set(SIMPLE_PROCESS_NAME, status.getSimpleProcessName()); builder.set(TASK, status.getTask()); builder.set(USER_NAME, status.getUserName()); builder.set(ASYNC, status.isAsynchronous()); Throwable exception = status.getException(); if (exception != null) { builder.set(EXCEPTION_CLASS, exception.getClass().getName()); builder.set(EXCEPTION_MESSAGE, exception.getMessage()); StackTraceElement[] stackTrace = exception.getStackTrace(); StringBuffer buf = new StringBuffer(); for (StackTraceElement el : stackTrace) { buf.append(el.getClassName()).append(STACKTRACESEPERATOR); buf.append(el.getFileName()).append(STACKTRACESEPERATOR); buf.append(el.getMethodName()).append(STACKTRACESEPERATOR); buf.append(el.getLineNumber()); buf.append("\n"); } builder.set(STACK_TRACE, buf.toString().getBytes(Charset.forName("UTF-8"))); } SimpleFeature feature = builder.buildFeature(null); return feature; } private byte[] serializeRequest(ExecuteType request) { ByteArrayOutputStream out = new ByteArrayOutputStream(); Encoder e = new Encoder(new WPSConfiguration()); e.setIndenting(true); try { e.encode(request, WPS.Execute, out); } catch (IOException ex) { LOGGER.log(Level.INFO, "Problem encountered encoding WPS Request, moving on without it", ex); } return out.toByteArray(); } protected ExecutionStatus featureToStatus(SimpleFeature f) { HashMap<String, Object> attrs = new HashMap<>(); if (f == null) { return null; } for (Property p : f.getProperties()) { // if (p.getValue() != null) { attrs.put(p.getName().toString(), p.getValue()); } } Name processName = new NameImpl((String) attrs.get(PROCESS_NAME)); String executionId = (String) attrs.get(PROCESS_ID); boolean asynchronous = Converters.convert(attrs.get(ASYNC), Boolean.class); ExecutionStatus status = new ExecutionStatus(processName, executionId, asynchronous); if (attrs.containsKey(REQUEST)) { ExecuteType request = buildRequest(attrs); status.setRequest(request); } String phase = (String) attrs.get(PHASE); ProcessState state = ProcessState.valueOf(phase); status.setPhase(state); status.setProgress((float) attrs.get(PROGRESS)); status.setTask((String) attrs.get(TASK)); status.setUserName((String) attrs.get(USER_NAME)); if (attrs.containsKey(EXCEPTION_MESSAGE)) { status.setException(buildException(attrs, status)); } // set the time stamps last as other items may set them for us but // we know best here! status.setCreationTime((Date) attrs.get(CREATION)); if (attrs.containsKey(COMPLETION)) { status.setCompletionTime((Date) attrs.get(COMPLETION)); } if (attrs.containsKey(LASTUPDATE)) { status.setLastUpdated((Date) attrs.get(LASTUPDATE)); } return status; } private ExecuteType buildRequest(HashMap<String, Object> attrs) { byte[] req = (byte[]) attrs.get(REQUEST); org.geotools.xml.Parser parser = new Parser(new WPSConfiguration()); ExecuteType request = null; try { request = (ExecuteType) parser.parse(new ByteArrayInputStream(req)); } catch (IOException | SAXException | ParserConfigurationException e) { LOGGER.log(Level.WARNING, "Problem building WPS request for status", e); } return request; } private Exception buildException(HashMap<String, Object> attrs, ExecutionStatus status) { String message = (String) attrs.get(EXCEPTION_MESSAGE); Exception exc = new Exception(message); // see if we can rebuild the exception try { Constructor<?> con = this.getClass().getClassLoader().loadClass((String) attrs.get(EXCEPTION_CLASS)) .getConstructor(String.class); exc = (Exception) con.newInstance(message); } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) { // too bad, I don't care LOGGER.log(Level.FINE, "Couldn't reinstaniate Exception for WPS status", e); } byte[] r = (byte[]) attrs.get(STACK_TRACE); ArrayList<StackTraceElement> trace = new ArrayList<>(); for (String line : new String(r, Charset.forName("UTF-8")).split("\n")) { String[] parts = line.split(STACKTRACESEPERATOR); String declaringClass = parts[0]; String fileName = parts[1]; String methodName = parts[2]; int lineNumber = Integer.parseInt(parts[3]); StackTraceElement t = new StackTraceElement(declaringClass, methodName, fileName, lineNumber); trace.add(t); } exc.setStackTrace(trace.toArray(new StackTraceElement[] {})); return exc; } @Override public boolean supportsPredicate() { return false; } @Override public boolean supportsPaging() { return true; } }