package org.imixs.marty.plugins;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Logger;
import javax.xml.bind.DatatypeConverter;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.ItemCollectionComparator;
import org.imixs.workflow.WorkflowKernel;
import org.imixs.workflow.engine.WorkflowService;
import org.imixs.workflow.engine.plugins.AbstractPlugin;
import org.imixs.workflow.engine.plugins.VersionPlugin;
import org.imixs.workflow.exceptions.PluginException;
import org.imixs.workflow.exceptions.QueryException;
/**
* The DMS Plug-in stores attached files of a workitem into a separated
* BlobWorkitem and stores DMS meta data into the workitem.
*
* The Plug-in computes the read and write access for the BlobWorkitem based on
* the ACL of the processed workItem. The Plug-in should run immediate after the
* AccessPlugin.
*
* The Plug-in calculates a MD5 checksum for the file content.
*
* <br>
* <br>
* The Plug-in only runs if workItem type is 'workitem' or 'workitemarchive'
*
* <br>
* <br>
* The plug-in provides additional static methods to set and get the DMS meta
* data for a workitem. The DMS meta data is stored in the property "dms". This
* property provides a list of Map objects containing the dms meta data. The
* method getDMSList can be used to convert this list into a List of
* ItemCollection elements.
*
* <br>
* <br>
* In addition the DMS Plug-in also provides a mechanism to import files from
* the file system. If the workitem contains the property 'txtDmsImport" all
* files from the given path will be added into the blobWorkitem <br>
* <br>
* The DMS Plug-in provides the following attriubtes: <br>
* <ul>
* <li>dms : meta data for files</li>
* <li>dms_names : list of all file names (for lucene search)</li>
* <li>dms_ocr : ocr text for each file (not yet used)</li>
* </ul>
*
* @author rsoika
* @version 2.0
*/
public class DMSPlugin extends AbstractPlugin {
public final static String DMS_ITEM = "dms";
public final static String DMS_FILE_NAMES = "dms_names"; // list of files
public final static String DMS_FILE_COUNT = "dms_count"; // count of files
public final static String DMS_FILE_OCR = "dms_ocr"; // not yet in use!
public final static String DMS_IMPORT_PROPERTY = "txtDmsImport";
public final static String DEFAULT_PROTOCOLL = "file://";
public final static String BLOBWORKITEMID = "$BlobWorkitem";
public final static String CHECKSUM_ERROR = "CHECKSUM_ERROR";
public final static String FILE_IMPORT_ERROR = "FILE_IMPORT_ERROR";
ItemCollection workitem = null;
private ItemCollection blobWorkitem = null;
private static Logger logger = Logger.getLogger(DMSPlugin.class.getName());
/**
* Update the read and writeAccess of the blobWorkitem and generates the dms
* meta data item. If no blobWorkitem still exists for the current workitem,
* the method creates the blobWorkitem automatically.
*
* If a file is contained in the property '$file', which is not yet part of
* the property 'dms' the method will automatically create a new dms entry
* and calculates a MD5 checksum for the file content. <br>
* <br>
* If the workItem contains the property 'txtDmsImport" all files from the
* given path will be added into the blobWorkitem
*
*
* @throws PluginException
* @version 1.0s
**/
@SuppressWarnings("unchecked")
@Override
public ItemCollection run(ItemCollection documentContext, ItemCollection documentActivity) throws PluginException {
workitem = documentContext;
// skip if type is not workitem and workitemarchive
if (!workitem.getType().equals("workitem") && !workitem.getType().equals("workitemarchive")) {
return workitem;
}
// Skip if plugin this processing a new version created by the version
// plugin - in this case no changes to the version are needed
if (VersionPlugin.isProcssingVersion(workitem)) {
return workitem;
}
// load the blobWorkitem and update read- and write access
blobWorkitem = loadBlobWorkitem(workitem);
if (blobWorkitem == null) {
logger.warning(
"can't access blobworkitem for '" + workitem.getUniqueID() + "'");
return workitem;
}
// import files if txtDmsImport is defined.
if (documentContext.hasItem(DMS_IMPORT_PROPERTY)) {
importFilesFromPath(workitem, blobWorkitem, documentContext.getItemValue(DMS_IMPORT_PROPERTY));
// remove property
documentContext.removeItem(DMS_IMPORT_PROPERTY);
}
// update the dms list - e.g. if another plugin had added a file....
// the blob workitem will only be saved in case any changes were
// performed...
try {
boolean updateBlob = false;
updateBlob = updateDmsList(workitem, blobWorkitem, this.getWorkflowService().getUserName());
// Update Read and write access list from parent workItem
if (!workitem.getItemValue(WorkflowService.READACCESS)
.equals(blobWorkitem.getItemValue(WorkflowService.READACCESS))) {
blobWorkitem.replaceItemValue(WorkflowService.READACCESS,
workitem.getItemValue(WorkflowService.READACCESS));
updateBlob = true;
}
if (!workitem.getItemValue(WorkflowService.WRITEACCESS)
.equals(blobWorkitem.getItemValue(WorkflowService.WRITEACCESS))) {
blobWorkitem.replaceItemValue(WorkflowService.WRITEACCESS,
workitem.getItemValue(WorkflowService.WRITEACCESS));
updateBlob = true;
}
// save BlobWorkitem...
if (updateBlob) {
logger.fine(
"saving blobWorkitem '" + blobWorkitem.getUniqueID() + "'...");
blobWorkitem = getWorkflowService().getDocumentService().save(blobWorkitem);
}
} catch (NoSuchAlgorithmException e) {
logger.severe("failed to compute MD5 checksum: " + documentContext.getUniqueID() + " - " + e.getMessage());
throw new PluginException(DMSPlugin.class.getSimpleName(), CHECKSUM_ERROR,
"failed to compute MD5 checksum: " + documentContext.getUniqueID() + "(" + e.getMessage() + ")", e);
}
logger.fine("updating $readaccess/$writeaccess for " + workitem.getUniqueID());
// update property '$BlobWorkitem'
workitem.replaceItemValue(BLOBWORKITEMID, blobWorkitem.getUniqueID());
// add $filecount
workitem.replaceItemValue(DMS_FILE_COUNT, workitem.getFileNames().size());
// add $filenames
workitem.replaceItemValue(DMS_FILE_NAMES, workitem.getFileNames());
return workitem;
}
/**
* This method returns a list of ItemCollections for all DMS elements
* attached to the current workitem. The DMS meta data is read from the
* property 'dms'.
*
* The dms property is updated in the run() method of this plug-in.
*
* The method is used by the DmsController to display the dms meta data.
*
* @param workitem
* - source of meta data, sorted by $creation
* @version 1.0
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public static List<ItemCollection> getDmsList(ItemCollection workitem) {
// build a new fileList and test if each file contained in the $files is
// listed
List<ItemCollection> dmsList = new ArrayList<ItemCollection>();
if (workitem == null)
return dmsList;
List<Map> vDMS = workitem.getItemValue(DMS_ITEM);
// first we add all existing dms informations
for (Map aMetadata : vDMS) {
dmsList.add(new ItemCollection(aMetadata));
}
// sort list by name
// Collections.sort(dmsList, new ItemCollectionComparator("txtname",
// true));
// sort list by $modified
Collections.sort(dmsList, new ItemCollectionComparator("$created", true));
return dmsList;
}
/**
* This method converts a list of ItemCollections for DMS elements into Map
* objects and updates the workitem property 'dms'.
*
* The method is used by the DmsController to update dms data provided by
* the user.
*
* @param workitem
* - the workitem to be updated
* @param dmsList
* - the dms metha data to be put into the workitem
* @version 1.0
*/
@SuppressWarnings("rawtypes")
public static void putDmsList(ItemCollection workitem, List<ItemCollection> dmsList) {
// convert the List<ItemCollection> into a List<Map>
List<Map> vDMSnew = new ArrayList<Map>();
if (dmsList != null) {
for (ItemCollection dmsEntry : dmsList) {
vDMSnew.add(dmsEntry.getAllItems());
}
}
// update the workitem
workitem.replaceItemValue(DMS_ITEM, vDMSnew);
}
/**
* This method adds a new entry into the dms property. The method returns
* the updated DMS List.
*
* The method is used by the DMSController to add links.
*
* @param aworkitem
* - the workitem to be updated
* @param dmsEntity
* - the metha data to be added into the dms item
* @version 1.0
*/
public static List<ItemCollection> addDMSEntry(ItemCollection aworkitem, ItemCollection dmsEntity) {
List<ItemCollection> dmsList = DMSPlugin.getDmsList(aworkitem);
String sNewName = dmsEntity.getItemValueString("txtName");
String sNewUrl = dmsEntity.getItemValueString("url");
// test if the entry already exists - than overwrite it....
for (Iterator<ItemCollection> iterator = dmsList.iterator(); iterator.hasNext();) {
ItemCollection admsEntry = iterator.next();
String sName = admsEntry.getItemValueString("txtName");
String sURL = admsEntry.getItemValueString("url");
if (sURL.endsWith(sNewUrl) && sName.equals(sNewName)) {
// Remove the current element from the iterator and the list.
iterator.remove();
logger.fine("remove dms entry '" + sName + "'");
}
}
dmsList.add(dmsEntity);
putDmsList(aworkitem, dmsList);
return dmsList;
}
/**
* This method transfers new file content into the blobWorkitem and updates
* the property 'dms' of the current workitem with the meta data of attached
* files or links.
*
* This method creates new empty DMS entries in the current workitem for new
* uploaded files which are still not contained in the dms item list. The
* workitem will only hold a empty byte array for files.
*
* If a new file content was added, the MD5 checksum will be generated.
*
* If the content of the blobWorkitem was updated, the method returns true.
*
* @param aWorkitem
* @param dmsList
* - map with meta information for each file entry
* @param defaultUsername
* - default username for new dms entries
* @return true if the dms item was changed
* @throws NoSuchAlgorithmException
*
*/
private boolean updateDmsList(ItemCollection aWorkitem, ItemCollection blobWorkitem, String defaultUsername)
throws NoSuchAlgorithmException {
boolean updateBlob = false;
List<ItemCollection> currentDmsList = getDmsList(aWorkitem);
List<String> fileNames = aWorkitem.getFileNames();
Map<String, List<Object>> files = aWorkitem.getFiles();
// first we remove all DMS entries which did not have a matching
// $File-Entry and are not from type link
if (fileNames == null) {
fileNames = new ArrayList<String>();
}
for (Iterator<ItemCollection> iterator = currentDmsList.iterator(); iterator.hasNext();) {
ItemCollection dmsEntry = iterator.next();
String sName = dmsEntry.getItemValueString("txtName");
String sURL = dmsEntry.getItemValueString("url");
if (sURL.isEmpty() && !fileNames.contains(sName)) {
// Remove the current element from the iterator and the list.
logger.fine("remove dms entry '" + sName + "'");
iterator.remove();
// update = true;
}
}
// now we test for each file entry if a dms meta data entry
// exists. If not we create a new one...
if (files != null) {
for (Entry<String, List<Object>> entry : files.entrySet()) {
String aFilename = entry.getKey();
List<?> file = entry.getValue();
// if data size >0 transfer file into blob and create a MD5
// Checksum
if (file.size() >= 2) {
String contentType = (String) file.get(0);
byte[] fileContent = (byte[]) file.get(1);
if (fileContent != null && fileContent.length > 1) {
// move...
blobWorkitem.addFile(fileContent, aFilename, contentType);
// empty data...
byte[] empty = { 0 };
// add the file name (with empty data) into the
// parentWorkitem.
aWorkitem.addFile(empty, aFilename, contentType);
updateBlob = true;
}
}
ItemCollection dmsEntry = findDMSEntry(aFilename, currentDmsList);
if (dmsEntry == null) {
// no meta data exists.... create a new meta object
dmsEntry = new ItemCollection();
dmsEntry.replaceItemValue("txtname", aFilename);
dmsEntry.replaceItemValue("$uniqueidRef", blobWorkitem.getUniqueID());
dmsEntry.replaceItemValue("$created", new Date());
dmsEntry.replaceItemValue("namCreator", defaultUsername);// deprecated
dmsEntry.replaceItemValue("$Creator", defaultUsername);
dmsEntry.replaceItemValue("txtcomment", "");
// compute md5 checksum
byte[] fileContent = (byte[]) file.get(1);
dmsEntry.replaceItemValue("md5Checksum", generateMD5(fileContent));
currentDmsList.add(dmsEntry);
} else {
// dms entry exists. We update if new file content was added
byte[] fileContent = (byte[]) file.get(1);
if (fileContent != null && fileContent.length > 1) {
dmsEntry.replaceItemValue("md5Checksum", generateMD5(fileContent));
dmsEntry.replaceItemValue("$modified", new Date());
dmsEntry.replaceItemValue("$editor", defaultUsername);
dmsEntry.replaceItemValue("$uniqueidRef", blobWorkitem.getUniqueID());
// update dmsEntry in dmsList..
// ??? currentDmsList.f..remove(o)
}
}
}
}
// now we remove all files form the blobWorkitem which are not part of
// the workitem
List<String> currentFileNames = aWorkitem.getFileNames();
List<String> blobFileNames = blobWorkitem.getFileNames();
List<String> removeList = new ArrayList<String>();
for (String aname : blobFileNames) {
if (!currentFileNames.contains(aname)) {
removeList.add(aname);
}
}
if (removeList.size() > 0) {
for (String aname : removeList) {
blobWorkitem.removeFile(aname);
}
updateBlob = true;
}
// finally update the modified dms list....
putDmsList(aWorkitem, currentDmsList);
return updateBlob;
}
/**
* This method loads the BlobWorkitem for a given parent WorkItem. The
* BlobWorkitem is identified by the $unqiueidRef.
*
* If no BlobWorkitem still exists the method creates a new empty
* BlobWorkitem which can be saved later.
*
* @param parentWorkitem
* - the corresponding parent workitem
* @version 1.0
*/
private ItemCollection loadBlobWorkitem(ItemCollection parentWorkitem) {
ItemCollection blobWorkitem = null;
// is parentWorkitem defined?
if (parentWorkitem == null) {
logger.warning("Unable to load blobWorkitem from parent workitem == null!");
return null;
}
// try to load the blobWorkitem with the parentWorktiem reference....
String sUniqueID = parentWorkitem.getUniqueID();
if (!"".equals(sUniqueID)) {
// search entity...
String sQuery = "(type:\"workitemlob\" AND $uniqueidref:\"" + sUniqueID + "\")";
Collection<ItemCollection> itemcol = null;
try {
itemcol = getWorkflowService().getDocumentService().find(sQuery, 1, 0);
} catch (QueryException e) {
logger.severe("loadBlobWorkitem - invalid query: " + e.getMessage());
}
// if blobWorkItem was found return...
if (itemcol != null && itemcol.size() > 0) {
blobWorkitem = itemcol.iterator().next();
}
} else {
logger.fine("generating inital $uniqueId for new parent workitem...");
// no $uniqueId set - create a UniqueID for the parentWorkitem
parentWorkitem.replaceItemValue(WorkflowKernel.UNIQUEID, WorkflowKernel.generateUniqueID());
}
// if no blobWorkitem was found, create a empty itemCollection..
if (blobWorkitem == null) {
logger.fine("creating new blobWorkitem...");
blobWorkitem = new ItemCollection();
blobWorkitem.replaceItemValue("type", "workitemlob");
// generate default uniqueid...
blobWorkitem.replaceItemValue(WorkflowKernel.UNIQUEID, WorkflowKernel.generateUniqueID());
blobWorkitem.replaceItemValue("$UniqueidRef", parentWorkitem.getUniqueID());
}
return blobWorkitem;
}
/**
* This method returns the meta data of a specific file in the exiting
* filelist.
*
* @return
*/
private static ItemCollection findDMSEntry(String aFilename, List<ItemCollection> dmsList) {
for (ItemCollection dmsEntry : dmsList) {
// test if filename matches...
String sName = dmsEntry.getItemValueString("txtname");
if (sName.equals(aFilename))
return dmsEntry;
}
// no matching meta data found!
return null;
}
/**
* Generates a MD5 from a byte array
*
* @param b
* @return
* @throws NoSuchAlgorithmException
*/
private static String generateMD5(byte[] b) throws NoSuchAlgorithmException {
byte[] hash_bytes = MessageDigest.getInstance("MD5").digest(b);
return DatatypeConverter.printHexBinary(hash_bytes);
}
/**
* Import files from a given location.
*
* @param aWorkitem
* @param importList
* - list of files
* @throws PluginException
*
*/
private void importFilesFromPath(ItemCollection adocumentContext, ItemCollection blobWorkitem,
List<String> importList) throws PluginException {
for (String fileUri : importList) {
if (fileUri.isEmpty()) {
continue;
}
String fullFileUri = fileUri;
String fileName = null;
// check for a protocoll
if (!fullFileUri.contains("://")) {
fullFileUri = DEFAULT_PROTOCOLL + fullFileUri;
}
// extract fileame....
if (!fullFileUri.contains("/")) {
continue;
}
fileName = fullFileUri.substring(fullFileUri.lastIndexOf("/") + 1);
logger.info("importFilesFromPath: " + fullFileUri);
try {
URL url = new URL(fullFileUri);
ByteArrayOutputStream bais = new ByteArrayOutputStream();
InputStream is = null;
try {
is = url.openStream();
byte[] byteChunk = new byte[4096]; // Or whatever size you
// want to read in at a
// time.
int n;
while ((n = is.read(byteChunk)) > 0) {
bais.write(byteChunk, 0, n);
}
} catch (IOException e) {
logger.severe("Failed while reading bytes from " + url.toExternalForm());
e.printStackTrace();
// Perform any other exception handling that's appropriate.
} finally {
if (is != null) {
is.close();
}
}
blobWorkitem.addFile(bais.toByteArray(), fileName, "");
// add the file name (with empty data) into the
// parentWorkitem.
byte[] empty = { 0 };
adocumentContext.addFile(empty, fileName, "");
logger.info("file import successfull ");
} catch (MalformedURLException e) {
throw new PluginException(DMSPlugin.class.getSimpleName(), FILE_IMPORT_ERROR,
"error importing files from: " + fullFileUri, e);
} catch (IOException e) {
throw new PluginException(DMSPlugin.class.getSimpleName(), FILE_IMPORT_ERROR,
"error importing files from: " + fullFileUri, e);
}
}
}
}