/* * Copyright (c) 2004-2020 The YAWL Foundation. All rights reserved. * The YAWL Foundation is a collaboration of individuals and * organisations who are committed to improving workflow technology. * * This file is part of YAWL. YAWL is free software: you can * redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation. * * YAWL 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 Lesser General * Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with YAWL. If not, see <http://www.gnu.org/licenses/>. */ package org.yawlfoundation.yawl.documentStore; import org.apache.logging.log4j.Logger; import org.hibernate.ObjectNotFoundException; import org.yawlfoundation.yawl.engine.interfce.YHttpServlet; import org.yawlfoundation.yawl.util.HibernateEngine; import org.yawlfoundation.yawl.util.Sessions; import org.yawlfoundation.yawl.util.StringUtil; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.sql.*; import java.util.Enumeration; import java.util.HashSet; import java.util.Properties; import java.util.Set; /** * A storage cache for binary files passed as work item data. * * @author Michael Adams * @date 18/11/11 */ public class DocumentStore extends YHttpServlet { private Sessions _sessions; // maintains sessions with external services private HibernateEngine _db; // communicates with underlying database private boolean _retainWhenCaseCompletes; public void init() { ServletContext context = getServletContext(); // check size-fix for H2 databases fixH2BinarySize(context); // setup database connection Set<Class> persistedClasses = new HashSet<Class>(); persistedClasses.add(YDocument.class); _db = new HibernateEngine(true, persistedClasses); // set up session connections _sessions = new Sessions(); _sessions.setupInterfaceA( context.getInitParameter("InterfaceA_Backend"), context.getInitParameter("EngineLogonUserName"), context.getInitParameter("EngineLogonPassword")); // set retention flag String retain = context.getInitParameter("RetainStoredDocsOnCaseCompletion"); _retainWhenCaseCompletes = (retain != null) && retain.equalsIgnoreCase("true"); } public void destroy() { if (_db != null) _db.closeFactory(); if (_sessions != null) _sessions.shutdown(); super.destroy(); } public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { doPost(req, res); // redirect all GETs to POSTs } public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException { try { // all request parameters are passed via the request's input stream DataInputStream dis = new DataInputStream( new BufferedInputStream(req.getInputStream())); String action = dis.readUTF(); String handle = dis.readUTF(); String result = null; if (action == null) { throw new IOException("action is null"); } else if (action.equals("connect")) { String userid = dis.readUTF(); String password = dis.readUTF(); result = _sessions.connect(userid, password); } else if (action.equals("checkConnection")) { result = String.valueOf(_sessions.checkConnection(handle)); } else if (action.equals("disconnect")) { result = String.valueOf(_sessions.disconnect(handle)); } else if (_sessions.checkConnection(handle)) { String caseID = dis.readUTF(); long docID = dis.readLong(); if (action.equals("get")) { writeDocument(res, getDocument(docID)); } else if (action.equals("put")) { result = String.valueOf(putDocument(new YDocument(caseID, docID, dis))); } else if (action.equals("remove")) { result = String.valueOf(removeDocument(docID)); } else if (action.equals("clearcase")) { result = clearCase(caseID); } else if (action.equals("addcaseid")) { result = addCaseID(docID, caseID); } else if (action.equals("completecase")) { if (_retainWhenCaseCompletes) { writeString(res, "Documents not cleared: configured to retain on case completion", "failure"); } else result = clearCase(caseID); } } else writeString(res, "Invalid or disconnected session handle", "failure"); if (result != null) writeString(res, result, "response"); } catch (EOFException eofe) { // occurs when the inputStream is null writeString(res, "Welcome to the YAWL Document Store Service", "response"); } catch (IOException ioe) { writeString(res, ioe.getMessage(), "failure"); } } /** * Writes a binary file to a response's output stream * * @param res the response * @param doc a YDocument wrapper containing the binary file * @throws IOException if there's a problem writing to the stream */ private void writeDocument(HttpServletResponse res, YDocument doc) throws IOException { if (doc != null) { res.setContentType("multipart/form-data"); res.setBufferSize(doc.getDocumentSize()); ServletOutputStream out = res.getOutputStream(); out.write(doc.getDocument()); out.flush(); out.close(); } } /** * Writes a UTF-8 String to a response's output stream * * @param res the response * @param msg the message to write * @param tag the xml tag to wrap the message in * @throws IOException if there's a problem writing to the stream */ private void writeString(HttpServletResponse res, String msg, String tag) throws IOException { if (msg != null) { res.setContentType("text/xml; charset=UTF-8"); OutputStreamWriter out = new OutputStreamWriter(res.getOutputStream(), "UTF-8"); out.write(StringUtil.wrap(msg, tag)); out.flush(); out.close(); } } /** * Reads a document from the database * * @param id the id of the document to read * @return a YDocument wrapper for the document * @throws IOException if no document can be found with the id passed */ private YDocument getDocument(long id) throws IOException { try { return (YDocument) _db.load(YDocument.class, id); } catch (ObjectNotFoundException onfe) { throw new IOException("No stored document found with id: " + id); } } /** * Writes a document to the database * * @param doc a YDocument wrapper for the document to write * @return the id (primary key) of the stored document * @throws IOException if the document can't be read from the request stream */ private long putDocument(YDocument doc) throws IOException { if (doc.getDocumentSize() > 0) { if (doc.hasValidId()) { // getDocument will propagate an exception if the id is unknown YDocument existingDoc = getDocument(doc.getId()); existingDoc.setDocument(doc.getDocument()); _db.exec(existingDoc, HibernateEngine.DB_UPDATE, true); } else { _db.exec(doc, HibernateEngine.DB_INSERT, true); } return doc.getId(); } else throw new IOException("Could not read document from request stream"); } /** * Removes a document from the database * * @param id the id of the document to remove * @return true if successful */ private boolean removeDocument(long id) { try { YDocument doc = (YDocument) _db.load(YDocument.class, id); return (doc != null) && _db.exec(doc, HibernateEngine.DB_DELETE, true); } catch (ObjectNotFoundException onfe) { return false; } } private String addCaseID(long id, String caseID) throws IOException { try { YDocument doc = (YDocument) _db.load(YDocument.class, id); if (doc != null) { doc.setCaseId(caseID); if (_db.exec(doc, HibernateEngine.DB_UPDATE, true)) { return "Case ID successfully updated"; } } throw new IOException("No document found with id: " + id); } catch (ObjectNotFoundException onfe) { throw new IOException(onfe.getMessage()); } } /** * Removes all the documents from the database that match the case id passed * * @param id the case id to remove documents for * @return a message indicating success or otherwise */ private String clearCase(String id) { StringBuilder sb = new StringBuilder(64); sb.append("delete from YDocument as yd where yd.caseId='").append(id).append("'"); int rowsDeleted = _db.execUpdate(sb.toString(), true); sb.delete(0, sb.length()); if (rowsDeleted > -1) { sb.append(rowsDeleted).append(" document") .append(rowsDeleted > 1 ? "s " : " ") .append("removed for case: ") .append(id); } else { sb.append("Error removing documents for case: ").append(id); } return sb.toString(); } /** * Increases the maximum column length for stored documents in H2 databases from 255 * to 5Mb. * <p/> * H2 defaults to 255 chars for binary types, and this default went out * with the 2.3 release. This method (1) checks if we are using a H2 database, then * if so (2) checks the relevant column length, then if it is 255 (3) increases it * to 5Mb maximum and (4) writes a flag file so that the method short-circuits on * future executions (i.e. it only runs once). * * @param context the current servlet context */ private void fixH2BinarySize(ServletContext context) { // if the flag files already exists, go no further if (context.getResourceAsStream("/WEB-INF/classes/dbfixed.bin") != null) { return; } // get the db properties from hibernate.properties Properties p = loadHibernateProperties(context); if (p == null) return; // couldn't find hibernate.properties Connection connection = null; try { String dialect = p.getProperty("hibernate.dialect"); // proceed only if this is a H2 database if (dialect != null && dialect.equals("org.hibernate.dialect.H2Dialect")) { Class.forName(p.getProperty("hibernate.connection.driver_class")); String url = p.getProperty("hibernate.connection.url"); if (url == null) return; // nothing to connect to url = url.replace("${catalina.base}", System.getenv("CATALINA_HOME")); connection = DriverManager.getConnection(url); if (connection != null) { // get the YDOC column size, and increase if required DatabaseMetaData dbmd = connection.getMetaData(); ResultSet rs = dbmd.getColumns(null, null, "YDOCUMENT", "YDOC"); rs.next(); if (rs.getInt("COLUMN_SIZE") <= 255) { connection.createStatement().executeUpdate( "ALTER TABLE ydocument ALTER COLUMN ydoc varbinary(5242880)"); // write flag file for next time new File(context.getRealPath("WEB-INF/classes/dbfixed.bin")).createNewFile(); } connection.close(); } } } catch (Exception e) { // can't update } if (connection != null) { try { connection.close(); } catch (SQLException sqle) { // } } } private Properties loadHibernateProperties(ServletContext context) { try { // for enterprise builds, hibernate.properties is in the 'classes' dir InputStream is = context.getResourceAsStream("/WEB-INF/classes/hibernate.properties"); // for installer builds, its in the 'yawllib' dir if (is == null) { File f = new File(System.getenv("CATALINA_HOME") + "/yawllib/hibernate.properties"); if (f != null && f.exists()) { is = new FileInputStream(f); } } if (is != null) { Properties p = new Properties(); p.load(is); return p; } } catch (Exception fallthough) { } return null; } }