/** * Convenience method to check the transformability of a transformation * * @param reader content reader * @param writer content writer * @param options transformation options * @throws AlfrescoRuntimeException if the the transformation isn't supported */ protected void checkTransformable( ContentReader reader, ContentWriter writer, TransformationOptions options) { String sourceMimetype = getMimetype(reader); String targetMimetype = getMimetype(writer); long sourceSize = reader.getSize(); boolean transformable = isTransformable(sourceMimetype, sourceSize, targetMimetype, options); if (transformable == false) { // This method is only called once a transformer has been selected, so it should be able to // handle the mimetypes but might not be able to handle all the limits as it might be part of // of a complex (compound) transformer. So report the max size if set. long maxSourceSizeKBytes = getMaxSourceSizeKBytes(sourceMimetype, targetMimetype, options); boolean sizeOkay = maxSourceSizeKBytes < 0 || (maxSourceSizeKBytes > 0 && sourceSize <= maxSourceSizeKBytes * 1024); AlfrescoRuntimeException e = new UnsupportedTransformationException( "Unsupported transformation: " + getBeanName() + ' ' + sourceMimetype + " to " + targetMimetype + ' ' + (sizeOkay ? "" : transformerDebug.fileSize(sourceSize) + " > " + transformerDebug.fileSize(maxSourceSizeKBytes * 1024))); throw transformerDebug.setCause(e); } // it all checks out OK }
@Test public void putForZeroLengthFile() { ContentReader contentReader = Mockito.mock(ContentReader.class); Mockito.when(contentReader.getSize()).thenReturn(0L); boolean putResult = contentCache.put("", contentReader); assertFalse("Zero length files should not be cached", putResult); }
@Override public boolean put(String contentUrl, ContentReader source) { File cacheFile = createCacheFile(); // Copy the content from the source into a cache file if (source.getSize() > 0L) { source.getContent(cacheFile); // Add a record of the cached file to the in-memory cache. recordCacheEntries(contentUrl, cacheFile); return true; } return false; }
@Test public void putForNonEmptyFile() { ContentReader contentReader = Mockito.mock(ContentReader.class); Mockito.when(contentReader.getSize()).thenReturn(999000L); final String url = "store://some/url.bin"; boolean putResult = contentCache.put(url, contentReader); assertTrue("Non-empty files should be cached", putResult); ArgumentCaptor<File> cacheFileArg = ArgumentCaptor.forClass(File.class); Mockito.verify(contentReader).getContent(cacheFileArg.capture()); // Check cached item is recorded properly in ehcache Mockito.verify(lookupTable).put(Key.forUrl(url), cacheFileArg.getValue().getAbsolutePath()); Mockito.verify(lookupTable) .put(Key.forCacheFile(cacheFileArg.getValue().getAbsolutePath()), url); }
/** * Stream content implementation * * @param req The request * @param res The response * @param reader The reader * @param nodeRef The content nodeRef if applicable * @param propertyQName The content property if applicable * @param attach Indicates whether the content should be streamed as an attachment or not * @param modified Modified date of content * @param eTag ETag to use * @param attachFileName Optional file name to use when attach is <code>true</code> * @throws IOException */ public void streamContentImpl( WebScriptRequest req, WebScriptResponse res, ContentReader reader, final NodeRef nodeRef, final QName propertyQName, final boolean attach, final Date modified, String eTag, final String attachFileName, Map<String, Object> model) throws IOException { setAttachment(null, res, attach, attachFileName); // establish mimetype String mimetype = reader.getMimetype(); String extensionPath = req.getExtensionPath(); if (mimetype == null || mimetype.length() == 0) { mimetype = MimetypeMap.MIMETYPE_BINARY; int extIndex = extensionPath.lastIndexOf('.'); if (extIndex != -1) { String ext = extensionPath.substring(extIndex + 1); mimetype = mimetypeService.getMimetype(ext); } } res.setHeader(HEADER_ACCEPT_RANGES, "bytes"); try { boolean processedRange = false; String range = req.getHeader(HEADER_CONTENT_RANGE); final long size = reader.getSize(); final String encoding = reader.getEncoding(); if (attach) { final String finalMimetype = mimetype; eventPublisher.publishEvent( new EventPreparator() { @Override public Event prepareEvent(String user, String networkId, String transactionId) { String siteId = siteService.getSiteShortName(nodeRef); return new ContentEventImpl( ContentEvent.DOWNLOAD, user, networkId, transactionId, nodeRef.getId(), siteId, propertyQName.toString(), Client.webclient, attachFileName, finalMimetype, size, encoding); } }); } if (range == null) { range = req.getHeader(HEADER_RANGE); } if (range != null) { if (logger.isDebugEnabled()) logger.debug("Found content range header: " + range); // ensure the range header is starts with "bytes=" and process the range(s) if (range.length() > 6) { if (range.indexOf(',') != -1 && (nodeRef == null || propertyQName == null)) { if (logger.isInfoEnabled()) logger.info("Multi-range only supported for nodeRefs"); } else { HttpRangeProcessor rangeProcessor = new HttpRangeProcessor(contentService); processedRange = rangeProcessor.processRange( res, reader, range.substring(6), nodeRef, propertyQName, mimetype, req.getHeader(HEADER_USER_AGENT)); } } } if (processedRange == false) { if (logger.isDebugEnabled()) logger.debug("Sending complete file content..."); // set mimetype for the content and the character encoding for the stream res.setContentType(mimetype); res.setContentEncoding(encoding); // return the complete entity range res.setHeader( HEADER_CONTENT_RANGE, "bytes 0-" + Long.toString(size - 1L) + "/" + Long.toString(size)); res.setHeader(HEADER_CONTENT_LENGTH, Long.toString(size)); // set caching setResponseCache(res, modified, eTag, model); // get the content and stream directly to the response output stream // assuming the repository is capable of streaming in chunks, this should allow large files // to be streamed directly to the browser response stream. reader.getContent(res.getOutputStream()); } } catch (SocketException e1) { // the client cut the connection - our mission was accomplished apart from a little error // message if (logger.isInfoEnabled()) logger.info("Client aborted stream read:\n\tcontent: " + reader); } catch (ContentIOException e2) { if (logger.isInfoEnabled()) logger.info("Client aborted stream read:\n\tcontent: " + reader); } }
/** * @see * org.alfresco.repo.content.transform.ContentTransformer#transform(org.alfresco.service.cmr.repository.ContentReader, * org.alfresco.service.cmr.repository.ContentWriter, * org.alfresco.service.cmr.repository.TransformationOptions) */ public final void transform( ContentReader reader, ContentWriter writer, TransformationOptions options) throws ContentIOException { try { depth.set(depth.get() + 1); // begin timing long before = System.currentTimeMillis(); String sourceMimetype = reader.getMimetype(); String targetMimetype = writer.getMimetype(); // check options map if (options == null) { options = new TransformationOptions(); } try { if (transformerDebug.isEnabled()) { transformerDebug.pushTransform( this, reader.getContentUrl(), sourceMimetype, targetMimetype, reader.getSize(), options); } // Check the transformability checkTransformable(reader, writer, options); // Pass on any limits to the reader setReaderLimits(reader, writer, options); // Transform // MNT-12238: CLONE - CLONE - Upload of PPTX causes very high memory usage leading to system // instability // Limiting transformation up to configured amount of milliseconds to avoid very high RAM // consumption // and OOM during transforming problematic documents TransformationOptionLimits limits = getLimits(reader.getMimetype(), writer.getMimetype(), options); long timeoutMs = limits.getTimeoutMs(); if (!useTimeoutThread || (null == limits) || (-1 == timeoutMs)) { transformInternal(reader, writer, options); } else { Future<?> submittedTask = null; StreamAwareContentReaderProxy proxiedReader = new StreamAwareContentReaderProxy(reader); StreamAwareContentWriterProxy proxiedWriter = new StreamAwareContentWriterProxy(writer); try { submittedTask = getExecutorService() .submit(new TransformInternalCallable(proxiedReader, proxiedWriter, options)); submittedTask.get(timeoutMs + additionalThreadTimout, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { releaseResources(submittedTask, proxiedReader, proxiedWriter); throw new TimeoutException("Transformation failed due to timeout limit"); } catch (InterruptedException e) { releaseResources(submittedTask, proxiedReader, proxiedWriter); throw new InterruptedException( "Transformation failed, because the thread of the transformation was interrupted"); } catch (ExecutionException e) { Throwable cause = e.getCause(); if (cause instanceof TransformInternalCallableException) { cause = ((TransformInternalCallableException) cause).getCause(); } throw cause; } } // record time long after = System.currentTimeMillis(); recordTime(sourceMimetype, targetMimetype, after - before); } catch (ContentServiceTransientException cste) { // A transient failure has occurred within the content transformer. // This should not be interpreted as a failure and therefore we should not // update the transformer's average time. if (logger.isDebugEnabled()) { logger.debug( "Transformation has been transiently declined: \n" + " reader: " + reader + "\n" + " writer: " + writer + "\n" + " options: " + options + "\n" + " transformer: " + this); } // the finally block below will still perform tidyup. Otherwise we're done. // We rethrow the exception throw cste; } catch (UnsupportedTransformationException e) { // Don't record an error or even the time, as this is normal in compound transformations. transformerDebug.debug(" Failed", e); throw e; } catch (Throwable e) { // Make sure that this transformation gets set back i.t.o. time taken. // This will ensure that transformers that compete for the same transformation // will be prejudiced against transformers that tend to fail long after = System.currentTimeMillis(); recordError(sourceMimetype, targetMimetype, after - before); // Ask Tika to detect the document, and report back on if // the current mime type is plausible String differentType = getMimetypeService().getMimetypeIfNotMatches(reader.getReader()); // Report the error if (differentType == null) { transformerDebug.debug(" Failed", e); throw new ContentIOException( "Content conversion failed: \n" + " reader: " + reader + "\n" + " writer: " + writer + "\n" + " options: " + options.toString(false) + "\n" + " limits: " + getLimits(reader, writer, options), e); } else { transformerDebug.debug(" Failed: Mime type was '" + differentType + "'", e); if (this.retryTransformOnDifferentMimeType) { // MNT-11015 fix. // Set a new reader to refresh the input stream. reader = reader.getReader(); // set the actual file MIME type detected by Tika for content reader reader.setMimetype(differentType); // Get correct transformer according actual file MIME type and try to transform file // with // actual transformer ContentTransformer transformer = this.registry.getTransformer( differentType, reader.getSize(), targetMimetype, options); if (null != transformer) { transformer.transform(reader, writer, options); } else { transformerDebug.debug(" Failed", e); throw new ContentIOException( "Content conversion failed: \n" + " reader: " + reader + "\n" + " writer: " + writer + "\n" + " options: " + options.toString(false) + "\n" + " limits: " + getLimits(reader, writer, options) + "\n" + " claimed mime type: " + reader.getMimetype() + "\n" + " detected mime type: " + differentType + "\n" + " transformer not found" + "\n", e); } } else { throw new ContentIOException( "Content conversion failed: \n" + " reader: " + reader + "\n" + " writer: " + writer + "\n" + " options: " + options.toString(false) + "\n" + " limits: " + getLimits(reader, writer, options) + "\n" + " claimed mime type: " + reader.getMimetype() + "\n" + " detected mime type: " + differentType, e); } } } finally { transformerDebug.popTransform(); // check that the reader and writer are both closed if (reader.isChannelOpen()) { logger.error( "Content reader not closed by transformer: \n" + " reader: " + reader + "\n" + " transformer: " + this); } if (writer.isChannelOpen()) { logger.error( "Content writer not closed by transformer: \n" + " writer: " + writer + "\n" + " transformer: " + this); } } // done if (logger.isDebugEnabled()) { logger.debug( "Completed transformation: \n" + " reader: " + reader + "\n" + " writer: " + writer + "\n" + " options: " + options + "\n" + " transformer: " + this); } } finally { depth.set(depth.get() - 1); } }
/** * @see * org.alfresco.repo.content.transform.AbstractContentTransformer2#transformInternal(org.alfresco.service.cmr.repository.ContentReader, * org.alfresco.service.cmr.repository.ContentWriter, * org.alfresco.service.cmr.repository.TransformationOptions) */ @Override public void transformInternal( ContentReader reader, ContentWriter writer, TransformationOptions options) throws Exception { final String outputMimetype = writer.getMimetype(); final String outputFileExt = getMimetypeService().getExtension(outputMimetype); // We need to keep a reference to thrown exceptions as we're going to catch them and // then move on to the next transformer. In the event that they all fail, we will throw // the final exception. Exception transformationException = null; for (int i = 0; i < transformers.size(); i++) { int oneBasedCount = i + 1; ContentTransformer transf = transformers.get(i); ContentWriter currentWriter = null; File tempFile = null; try { if (logger.isDebugEnabled()) { logger.debug( "Transformation attempt " + oneBasedCount + " of " + transformers.size() + ": " + transf); } if (!transf.isTransformable( reader.getMimetype(), reader.getSize(), outputMimetype, options)) { throw new UnsupportedTransformationException( "Unsupported transformation: " + reader.getMimetype() + " to " + outputMimetype); } // We can't know in advance which transformer in the sequence will work - if any. // Therefore we can't write into the ContentWriter stream. // So make a temporary file writer with the current transformer name. tempFile = TempFileProvider.createTempFile( "FailoverTransformer_intermediate_" + transf.getClass().getSimpleName() + "_", "." + outputFileExt); currentWriter = new FileContentWriter(tempFile); currentWriter.setMimetype(outputMimetype); currentWriter.setEncoding(writer.getEncoding()); // attempt to transform transf.transform(reader, currentWriter, options); // TODO Could add a check for zero-length output and treat that as a failure // final long writtenSize = currentWriter.getSize(); } catch (Exception are) { if (transformationException == null) { transformationException = are; } if (logger.isDebugEnabled()) { logger.debug("Transformation " + oneBasedCount + " was unsuccessful."); if (i != transformers.size() - 1) { // We don't log the last exception as we're going to throw it. logger.debug("The below exception is provided for information purposes only.", are); } } // Set a new reader to refresh the input stream. reader = reader.getReader(); // and move to the next transformer continue; } // No need to close input or output streams // At this point the current transformation was successful i.e. it did not throw an exception. // Now we must copy the content from the temporary file into the ContentWriter stream. if (tempFile != null) { writer.putContent(tempFile); } if (logger.isInfoEnabled()) { logger.info("Transformation was successful"); } return; } // At this point we have tried all transformers in the sequence without apparent success. if (transformationException != null) { transformerDebug.debug(" No more transformations to failover to"); if (logger.isDebugEnabled()) { logger.debug( "All transformations were unsuccessful. Throwing first exception.", transformationException); } throw transformationException; } }
private AlfrescoRuntimeException signFile( final NodeRef nodeRefToSign, final DigitalSigningDTO signingDTO, final File alfTempDir, final String alias, final KeyStore ks, final PrivateKey key, final Certificate[] chain) { final String fileNameToSign = fileFolderService.getFileInfo(nodeRefToSign).getName(); File fileConverted = null; File tempDir = null; try { ContentReader fileToSignContentReader = getReader(nodeRefToSign); if (fileToSignContentReader != null) { String newName = null; // Check if document is PDF or transform it if (!MimetypeMap.MIMETYPE_PDF.equals(fileToSignContentReader.getMimetype())) { // Transform document in PDF document final ContentTransformer tranformer = contentTransformerRegistry.getTransformer( fileToSignContentReader.getMimetype(), fileToSignContentReader.getSize(), MimetypeMap.MIMETYPE_PDF, new TransformationOptions()); if (tranformer != null) { tempDir = new File(alfTempDir.getPath() + File.separatorChar + nodeRefToSign.getId()); if (tempDir != null) { tempDir.mkdir(); fileConverted = new File(tempDir, fileNameToSign + "_" + System.currentTimeMillis() + ".pdf"); if (fileConverted != null) { final ContentWriter newDoc = new FileContentWriter(fileConverted); if (newDoc != null) { newDoc.setMimetype(MimetypeMap.MIMETYPE_PDF); tranformer.transform(fileToSignContentReader, newDoc); fileToSignContentReader = new FileContentReader(fileConverted); final String originalName = (String) nodeService.getProperty(nodeRefToSign, ContentModel.PROP_NAME); newName = originalName.substring(0, originalName.lastIndexOf(".")) + ".pdf"; } } } } else { log.error( "[" + fileNameToSign + "] No suitable converter found to convert the document in PDF."); return new AlfrescoRuntimeException( "[" + fileNameToSign + "] No suitable converter found to convert the document in PDF."); } } // Convert PDF in PDF/A format final File pdfAFile = convertPdfToPdfA(fileToSignContentReader.getContentInputStream()); final PdfReader reader = new PdfReader(new FileInputStream(pdfAFile)); if (nodeRefToSign != null) { tempDir = new File(alfTempDir.getPath() + File.separatorChar + nodeRefToSign.getId()); if (tempDir != null) { tempDir.mkdir(); final File file = new File(tempDir, fileNameToSign); if (file != null) { final FileOutputStream fout = new FileOutputStream(file); final PdfStamper stp = PdfStamper.createSignature(reader, fout, '\0'); if (stp != null) { final PdfSignatureAppearance sap = stp.getSignatureAppearance(); if (sap != null) { sap.setCrypto(key, chain, null, PdfSignatureAppearance.WINCER_SIGNED); sap.setReason(signingDTO.getSignReason()); sap.setLocation(signingDTO.getSignLocation()); sap.setContact(signingDTO.getSignContact()); sap.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED); sap.setImageScale(1); // digital signature if (signingDTO.getSigningField() != null && !signingDTO.getSigningField().trim().equalsIgnoreCase("")) { Image img = null; if (signingDTO.getImage() != null) { final ContentReader imageContentReader = getReader(signingDTO.getImage()); final AcroFields af = reader.getAcroFields(); if (af != null) { final List<FieldPosition> positions = af.getFieldPositions(signingDTO.getSigningField()); if (positions != null && positions.size() > 0 && positions.get(0) != null && positions.get(0).position != null) { final BufferedImage newImg = scaleImage( ImageIO.read(imageContentReader.getContentInputStream()), BufferedImage.TYPE_INT_RGB, Float.valueOf(positions.get(0).position.getWidth()).intValue(), Float.valueOf(positions.get(0).position.getHeight()).intValue()); img = Image.getInstance(newImg, null); } else { log.error( "[" + fileNameToSign + "] The field '" + signingDTO.getSigningField() + "' doesn't exist in the document."); return new AlfrescoRuntimeException( "[" + fileNameToSign + "] The field '" + signingDTO.getSigningField() + "' doesn't exist in the document."); } } if (img == null) { img = Image.getInstance( ImageIO.read(imageContentReader.getContentInputStream()), null); } sap.setImage(img); } sap.setVisibleSignature(signingDTO.getSigningField()); } else { int pageToSign = 1; if (DigitalSigningDTO.PAGE_LAST.equalsIgnoreCase( signingDTO.getPages().trim())) { pageToSign = reader.getNumberOfPages(); } else if (DigitalSigningDTO.PAGE_SPECIFIC.equalsIgnoreCase( signingDTO.getPages().trim())) { if (signingDTO.getPageNumber() > 0 && signingDTO.getPageNumber() <= reader.getNumberOfPages()) { pageToSign = signingDTO.getPageNumber(); } else { throw new AlfrescoRuntimeException("Page number is out of bound."); } } if (signingDTO.getImage() != null) { final ContentReader imageContentReader = getReader(signingDTO.getImage()); // Resize image final BufferedImage newImg = scaleImage( ImageIO.read(imageContentReader.getContentInputStream()), BufferedImage.TYPE_INT_RGB, signingDTO.getSignWidth(), signingDTO.getSignHeight()); final Image img = Image.getInstance(newImg, null); sap.setImage(img); } if (signingDTO.getPosition() != null && !DigitalSigningDTO.POSITION_CUSTOM.equalsIgnoreCase( signingDTO.getPosition().trim())) { final Rectangle pageRect = reader.getPageSizeWithRotation(1); sap.setVisibleSignature( positionSignature( signingDTO.getPosition(), pageRect, signingDTO.getSignWidth(), signingDTO.getSignHeight(), signingDTO.getxMargin(), signingDTO.getyMargin()), pageToSign, null); } else { sap.setVisibleSignature( new Rectangle( signingDTO.getLocationX(), signingDTO.getLocationY(), signingDTO.getLocationX() + signingDTO.getSignWidth(), signingDTO.getLocationY() - signingDTO.getSignHeight()), pageToSign, null); } } stp.close(); NodeRef destinationNode = null; NodeRef originalDoc = null; boolean addAsNewVersion = false; if (signingDTO.getDestinationFolder() == null) { destinationNode = nodeRefToSign; nodeService.addAspect(destinationNode, ContentModel.ASPECT_VERSIONABLE, null); addAsNewVersion = true; } else { originalDoc = nodeRefToSign; destinationNode = createDestinationNode( file.getName(), signingDTO.getDestinationFolder(), nodeRefToSign); } if (destinationNode != null) { final ContentWriter writer = contentService.getWriter(destinationNode, ContentModel.PROP_CONTENT, true); if (writer != null) { writer.setEncoding(fileToSignContentReader.getEncoding()); writer.setMimetype("application/pdf"); writer.putContent(file); file.delete(); if (fileConverted != null) { fileConverted.delete(); } nodeService.addAspect( destinationNode, SigningModel.ASPECT_SIGNED, new HashMap<QName, Serializable>()); nodeService.setProperty( destinationNode, SigningModel.PROP_REASON, signingDTO.getSignReason()); nodeService.setProperty( destinationNode, SigningModel.PROP_LOCATION, signingDTO.getSignLocation()); nodeService.setProperty( destinationNode, SigningModel.PROP_SIGNATUREDATE, new java.util.Date()); nodeService.setProperty( destinationNode, SigningModel.PROP_SIGNEDBY, AuthenticationUtil.getRunAsUser()); if (newName != null) { nodeService.setProperty(destinationNode, ContentModel.PROP_NAME, newName); } final X509Certificate c = (X509Certificate) ks.getCertificate(alias); nodeService.setProperty( destinationNode, SigningModel.PROP_VALIDITY, c.getNotAfter()); nodeService.setProperty( destinationNode, SigningModel.PROP_ORIGINAL_DOC, originalDoc); if (!addAsNewVersion) { if (!nodeService.hasAspect(originalDoc, SigningModel.ASPECT_ORIGINAL_DOC)) { nodeService.addAspect( originalDoc, SigningModel.ASPECT_ORIGINAL_DOC, new HashMap<QName, Serializable>()); } nodeService.createAssociation( originalDoc, destinationNode, SigningModel.PROP_RELATED_DOC); } } } else { log.error("[" + fileNameToSign + "] Destination node is not a valid NodeRef."); return new AlfrescoRuntimeException( "[" + fileNameToSign + "] Destination node is not a valid NodeRef."); } } else { log.error("[" + fileNameToSign + "] Unable to get PDF appearance signature."); return new AlfrescoRuntimeException( "[" + fileNameToSign + "] Unable to get PDF appearance signature."); } } else { log.error("[" + fileNameToSign + "] Unable to create PDF signature."); return new AlfrescoRuntimeException( "[" + fileNameToSign + "] Unable to create PDF signature."); } } } } else { log.error("[" + fileNameToSign + "] Unable to get document to sign content."); return new AlfrescoRuntimeException( "[" + fileNameToSign + "] Unable to get document to sign content."); } if (pdfAFile != null) { pdfAFile.delete(); } return null; } else { log.error("[" + fileNameToSign + "] The document has no content."); return new AlfrescoRuntimeException( "[" + fileNameToSign + "] The document has no content."); } } catch (KeyStoreException e) { log.error("[" + fileNameToSign + "] " + e); return new AlfrescoRuntimeException("[" + fileNameToSign + "] " + e.getMessage(), e); } catch (ContentIOException e) { log.error("[" + fileNameToSign + "] " + e); return new AlfrescoRuntimeException("[" + fileNameToSign + "] " + e.getMessage(), e); } catch (IOException e) { log.error("[" + fileNameToSign + "] " + e); return new AlfrescoRuntimeException("[" + fileNameToSign + "] " + e.getMessage(), e); } catch (DocumentException e) { log.error("[" + fileNameToSign + "] " + e); return new AlfrescoRuntimeException("[" + fileNameToSign + "] " + e.getMessage(), e); } finally { if (tempDir != null) { try { tempDir.delete(); } catch (Exception ex) { log.error("[" + fileNameToSign + "] " + ex); return new AlfrescoRuntimeException("[" + fileNameToSign + "] " + ex.getMessage(), ex); } } } }