package org.nightlabs.jfire.scripting.print.ui.transfer.delivery; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import javax.jdo.JDOHelper; import org.apache.log4j.Logger; import org.nightlabs.jdo.ObjectID; import org.nightlabs.jfire.scripting.id.ScriptRegistryItemID; import org.nightlabs.jfire.store.id.ProductID; import org.nightlabs.jfire.trade.editor2d.ILayout; import org.nightlabs.jfire.trade.editor2d.LayoutMapForArticleIDSet; import org.nightlabs.jfire.trade.id.ArticleID; import org.nightlabs.util.CacheDirTag; import org.nightlabs.util.IOUtil; /** * @author Daniel.Mazurek [at] NightLabs [dot] de * */ public abstract class AbstractScriptDataProviderThread extends Thread { private static final Logger logger = Logger.getLogger(AbstractScriptDataProviderThread.class); private AbstractClientDeliveryProcessorPrint clientDeliveryProcessor; // private AbstractClientDeliveryProcessorFactory clientDeliveryProcessorFactoryPrint; private volatile Throwable error; private Map<ProductID, Map<ScriptRegistryItemID, Object>> scriptingResult; private Object scriptingResultMutex = new Object(); /** * It's essential, that an item is always added to {@link #articleIDsReady} AND * removed from this list within the same synchronized block (on {@link #articleIDsMutex}). * That means, it must be kept in this list, until it is processed * completely. */ private LinkedList<ArticleID> articleIDsToProcess = new LinkedList<ArticleID>(); private LinkedList<ArticleID> articleIDsReady = new LinkedList<ArticleID>(); private Object articleIDsMutex = new Object(); public AbstractScriptDataProviderThread(AbstractClientDeliveryProcessorPrint clientDeliveryProcessor) { this.clientDeliveryProcessor = clientDeliveryProcessor; // this.clientDeliveryProcessorFactoryPrint = (AbstractClientDeliveryProcessorFactory) clientDeliveryProcessor.getClientDeliveryProcessorFactory(); this.articleIDsToProcess.addAll(this.clientDeliveryProcessor.getDelivery().getArticleIDs()); this.bulkProcessSize = clientDeliveryProcessor.getPreferredDocumentCount(); if (this.bulkProcessSize < 1) this.bulkProcessSize = 1; if (logger.isDebugEnabled()) logger.debug("New instance of TicketDataProviderThread created."); //$NON-NLS-1$ } // /** // * returns the Layout File of the ProductID // * // * @param productID the ProductID // * @return the Layout File of the ProductID // */ // public abstract File getLayoutFile(ProductID productID); /** * @return Returns <code>true</code>, if the method {@link #fetchReadyArticleID()} * would immediately return (either with an object result or <code>null</code>). * Returns <code>false</code>, if a call to {@link #fetchReadyArticleID()} * would block. */ public boolean canInvokeFetchReadyArticleIDWithoutBlocking() { synchronized (articleIDsMutex) { return articleIDsToProcess.isEmpty() || !articleIDsReady.isEmpty(); } } /** * If there is currently no articleID ready to be printed, but still articleIDs to be processed, * this method will block until one becomes ready. If there is nothing ready and nothing left * to be processed, this method returns <code>null</code>. * @see #canInvokeFetchReadyArticleIDWithoutBlocking() */ public ArticleID fetchReadyArticleID() throws Throwable { if (logger.isDebugEnabled()) logger.debug("fetchReadyArticleID: enter"); //$NON-NLS-1$ synchronized (articleIDsMutex) { if (articleIDsToProcess.isEmpty() && articleIDsReady.isEmpty()) { if (logger.isDebugEnabled()) logger.debug("fetchReadyArticleID: returning null"); //$NON-NLS-1$ return null; } if (logger.isDebugEnabled()) logger.debug("fetchReadyArticleID: beginning to wait"); //$NON-NLS-1$ while (error == null && articleIDsReady.isEmpty()) { try { articleIDsMutex.wait(10000); } catch (InterruptedException e) { // ignore } if (logger.isDebugEnabled() && articleIDsReady.isEmpty()) logger.debug("fetchReadyArticleID: still no articleID ready => continue waiting"); //$NON-NLS-1$ } if (error != null) throw error; if (logger.isDebugEnabled()) logger.debug("fetchReadyArticleID: removing and returning first ready article. articleIDsReady.size()=" + articleIDsReady.size()); //$NON-NLS-1$ return articleIDsReady.removeFirst(); } } public Throwable getError() { return error; } private volatile boolean interrupted = false; @Override public void interrupt() { interrupted = true; super.interrupt(); } @Override public boolean isInterrupted() { return interrupted || super.isInterrupted(); } protected boolean isArticleIDsToProcessEmpty() { synchronized (articleIDsMutex) { return articleIDsToProcess.isEmpty(); } } protected Map<ScriptRegistryItemID, Object> getScriptResultMap(ProductID ticketID, boolean throwExceptionIfNotFound) { Map<ScriptRegistryItemID, Object> scriptResultMap; synchronized (scriptingResultMutex) { scriptResultMap = scriptingResult.get(ticketID); } if (throwExceptionIfNotFound && scriptResultMap == null) throw new IllegalArgumentException("No scriptResult found for " + ticketID); //$NON-NLS-1$ return scriptResultMap; } // public abstract Map<ProductID, Map<ScriptRegistryItemID, Object>> getScriptingResult(); protected Map<ProductID, Map<ScriptRegistryItemID, Object>> getScriptingResult() { return scriptingResult; } // /** // * returns the corresponding {@link ProductID} for the given {@link ArticleID} // * // * @param articleID the {@link ArticleID} // * @param throwExceptionIfNotFound determines if a Exception should be thrown if no // * product could be found for the given articleID // * @return the corresponding {@link ProductID} for the given {@link ArticleID} // */ // public abstract ProductID getProductID(ArticleID articleID, boolean throwExceptionIfNotFound); protected ProductID getProductID(ArticleID articleID, boolean throwExceptionIfNotFound) { ProductID productID; synchronized (layoutMapForArticleIDSetMutex) { productID = layoutMapForArticleIDSet == null ? null : layoutMapForArticleIDSet.getArticleID2ProductIDMap().get(articleID); } if (throwExceptionIfNotFound && productID == null) throw new IllegalArgumentException("No artcileID found for " + articleID); //$NON-NLS-1$ return productID; } private Object layoutMapForArticleIDSetMutex = new Object(); private LayoutMapForArticleIDSet layoutMapForArticleIDSet; private File cacheDir; /** * If you inherit this class and override this method, you <b>must</b> call * <code>super.init();</code> in your implementation as first statement!!! */ public void init() { cacheDir = getCacheDir(); } public File getCacheDir() { if (cacheDir == null) { try { cacheDir = IOUtil.createUserTempDir("JFirePrintCache.", null); //$NON-NLS-1$ } catch (IOException e) { throw new RuntimeException(e); } CacheDirTag cacheDirTag = new CacheDirTag(cacheDir); try { cacheDirTag.tag("JFire delivery print cache", true, false); //$NON-NLS-1$ } catch (Exception e) { Logger.getLogger(AbstractScriptDataProviderThread.class).error("Tagging cache dir failed!", e); //$NON-NLS-1$ } } return cacheDir; } /** * This is initialized in the constructor to be the value returned by * {@link AbstractClientDeliveryProcessorPrint#getPreferredDocumentCount()}. * It will be double with each loop the thread runs until it reaches a * maximum of 12. */ private int bulkProcessSize; @Override public void run() { long runStart = System.currentTimeMillis(); if (logger.isDebugEnabled()) logger.debug("run: enter"); //$NON-NLS-1$ try { try { while (!isArticleIDsToProcessEmpty()) { // as long as there's sth. to do, we take the bulkProcessSize number of articleIDs and process them. if (logger.isDebugEnabled()) logger.debug("run: getting a bulk of max " + bulkProcessSize + " articleIDs to process:"); //$NON-NLS-1$ //$NON-NLS-2$ LinkedList<ArticleID> articleIDs = new LinkedList<ArticleID>(); synchronized (articleIDsMutex) { int i = 0; for (ArticleID articleID : articleIDsToProcess) { if (++i > bulkProcessSize) break; articleIDs.add(articleID); if (logger.isDebugEnabled()) logger.debug("run: " + articleID); //$NON-NLS-1$ } } if (logger.isDebugEnabled()) logger.debug("run: getLayoutMapForArticleIDSet from server..."); //$NON-NLS-1$ long localTimeMeasure = System.currentTimeMillis(); LayoutMapForArticleIDSet tlm = getLayoutMapForArticleIDSet(articleIDs); if (tlm == null) throw new IllegalStateException("getLayoutMapForArticleIDSet(articleIDs) returned null! Check your implementation in class: " + this.getClass().getName()); //$NON-NLS-1$ synchronized (layoutMapForArticleIDSetMutex) { if (layoutMapForArticleIDSet == null) layoutMapForArticleIDSet = tlm; else { layoutMapForArticleIDSet.append(tlm); } } DeliveryProcessorPrintDebugInfo.addTime( DeliveryProcessorPrintDebugInfo.CAT_FETCH_DATA_FETCH_LAYOUTS_MAP, System.currentTimeMillis() - localTimeMeasure); if (isInterrupted()) throw new InterruptedException("interrupted!"); //$NON-NLS-1$ // check which layout is not yet here or not up-to-date Set<ObjectID> layoutIDsToDownload = new HashSet<ObjectID>(); Collection<ILayout> layouts = tlm.getProductID2LayoutMap().values(); iterateTicketLayout: for (ILayout layout : layouts) { ObjectID layoutID = (ObjectID) JDOHelper.getObjectId(layout); File layoutFile = getLayoutFileByLayoutID(layoutID); if (!layoutFile.exists()) { if (logger.isDebugEnabled()) logger.debug("run: layoutFile does not exist: " + layoutFile); //$NON-NLS-1$ layoutIDsToDownload.add(layoutID); continue iterateTicketLayout; } if (!layoutFile.isFile()) throw new IllegalStateException("layoutFile exists, but is not a normal file: " + layoutFile); //$NON-NLS-1$ // the file exists, but is it up-to-date? if (Math.abs(layoutFile.lastModified() - layout.getFileTimestamp().getTime()) > 5000) { // file has different timestamp if (logger.isDebugEnabled()) logger.debug("run: layoutFile has different timestamp: " + layoutFile); //$NON-NLS-1$ layoutIDsToDownload.add(layoutID); continue iterateTicketLayout; } if (logger.isDebugEnabled()) logger.debug("run: layoutFile already exists with correct timestamp: " + layoutFile); //$NON-NLS-1$ } if (isInterrupted()) throw new InterruptedException("interrupted!"); //$NON-NLS-1$ // fetch all the necessary TicketLayouts WITH their files from the server if (!layoutIDsToDownload.isEmpty()) { localTimeMeasure = System.currentTimeMillis(); if (logger.isDebugEnabled()) logger.debug("run: loading complete Layouts (with files) from server..."); //$NON-NLS-1$ List<ILayout> ilayouts = getLayouts(layoutIDsToDownload); // store the files for (ILayout layout : ilayouts) { ObjectID layoutID = (ObjectID) JDOHelper.getObjectId(layout); File layoutFile = getLayoutFileByLayoutID(layoutID); if (logger.isDebugEnabled()) logger.debug("run: storing layoutFile: " + layoutFile); //$NON-NLS-1$ File dir = layoutFile.getParentFile(); if (!dir.exists()) { if (!dir.mkdirs()) throw new IOException("Creating directory failed: " + dir); //$NON-NLS-1$ } FileOutputStream out = new FileOutputStream(layoutFile); try { ByteArrayInputStream in = new ByteArrayInputStream(layout.getFileData()); IOUtil.transferStreamData(in, out); } finally { out.close(); } layoutFile.setLastModified(layout.getFileTimestamp().getTime()); } // for (TicketLayout ticketLayout : ticketLayouts) { DeliveryProcessorPrintDebugInfo.addTime( DeliveryProcessorPrintDebugInfo.CAT_FETCH_DATA_FETCH_LAYOUTS, System.currentTimeMillis() - localTimeMeasure); } // if (!ticketLayoutIDsToDownload.isEmpty()) { else { // nothing to download, only register debug info DeliveryProcessorPrintDebugInfo.addTime( DeliveryProcessorPrintDebugInfo.CAT_FETCH_DATA_FETCH_LAYOUTS, 0); } if (isInterrupted()) throw new InterruptedException("interrupted!"); //$NON-NLS-1$ if (logger.isDebugEnabled()) logger.debug("run: get ScriptingResult from server..."); //$NON-NLS-1$ localTimeMeasure = System.currentTimeMillis(); LinkedList<ProductID> productIDs = new LinkedList<ProductID>(); for (ArticleID articleID : articleIDs) { ProductID productID = layoutMapForArticleIDSet.getArticleID2ProductIDMap().get(articleID); if (productID == null) throw new IllegalStateException("No productID found for " + articleID); //$NON-NLS-1$ productIDs.add(productID); } Map<ProductID, Map<ScriptRegistryItemID, Object>> tsr = getScriptingResults(productIDs); synchronized (scriptingResultMutex) { if (scriptingResult == null) scriptingResult = tsr; else scriptingResult.putAll(tsr); // imho it's safe to do that, because no productID should ever occur twice } DeliveryProcessorPrintDebugInfo.addTime( DeliveryProcessorPrintDebugInfo.CAT_FETCH_DATA_SCRIPT_RESULTS, System.currentTimeMillis() - localTimeMeasure); if (isInterrupted()) throw new InterruptedException("interrupted!"); //$NON-NLS-1$ if (logger.isDebugEnabled()) logger.debug("run: transferring processed articles to ready list: "); //$NON-NLS-1$ // move the processed articleIDs from one list to the other synchronized (articleIDsMutex) { for (ArticleID articleID : articleIDs) { articleIDsReady.add(articleID); articleIDsToProcess.remove(articleID); // should be fast, because that should always be the first if (logger.isDebugEnabled()) logger.debug("run: " + articleID); //$NON-NLS-1$ } articleIDsMutex.notifyAll(); } bulkProcessSize *= 2; if (bulkProcessSize > 12) bulkProcessSize = 12; } // while (!isArticleIDsToProcessEmpty()) { } catch (Throwable t) { logger.error("Retrieving data failed!", t); //$NON-NLS-1$ error = t; } } finally { synchronized (articleIDsMutex) { articleIDsMutex.notifyAll(); } if (logger.isDebugEnabled()) { logger.debug("run: exit"); //$NON-NLS-1$ long duration = System.currentTimeMillis() - runStart; DeliveryProcessorPrintDebugInfo.addTime( DeliveryProcessorPrintDebugInfo.CAT_FETCH_DATA_TOTAL_TIME, duration); logger.debug("Retrieving data took "+duration); } } } protected abstract LayoutMapForArticleIDSet getLayoutMapForArticleIDSet(List<ArticleID> articleIDs); protected abstract Map<ProductID, Map<ScriptRegistryItemID, Object>> getScriptingResults( List<ProductID> productIDs); protected abstract File getLayoutFileByLayoutID(ObjectID layoutID); protected ObjectID getLayoutByProductID(ProductID productID) { ILayout layout; synchronized (layoutMapForArticleIDSetMutex) { layout = layoutMapForArticleIDSet.getProductID2LayoutMap().get(productID); } if (layout == null) throw new IllegalArgumentException("productID unknown: " + productID); //$NON-NLS-1$ return (ObjectID) JDOHelper.getObjectId(layout); } protected File getLayoutFileByProductID(ProductID productID) { ILayout layout; synchronized (layoutMapForArticleIDSetMutex) { layout = layoutMapForArticleIDSet.getProductID2LayoutMap().get(productID); } if (layout == null) throw new IllegalArgumentException("productID unknown: " + productID); //$NON-NLS-1$ ObjectID objectID = (ObjectID) JDOHelper.getObjectId(layout); return getLayoutFileByLayoutID(objectID); } protected abstract List<ILayout> getLayouts(Set<ObjectID> layoutIDs); }