diff --git a/config/alfresco/rendition-services-context.xml b/config/alfresco/rendition-services-context.xml index 3f7849ad8a..74719d0d30 100644 --- a/config/alfresco/rendition-services-context.xml +++ b/config/alfresco/rendition-services-context.xml @@ -1,147 +1,164 @@ - - - - - - - - - - org.alfresco.service.cmr.rendition.RenditionService - - - - - - - - - - - - - - - - - - - - - - ${server.transaction.mode.default} - - - - - - - - - - - - - - - - - - false - - - - - - - - - - - - - - - - - - - - - - - - alfresco.messages.rendition-config - - - - - - - - - - - - - - - - - - - - {http://www.alfresco.org/model/content/1.0}content - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + org.alfresco.service.cmr.rendition.RenditionService + + + + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + alfresco.messages.rendition-config + + + + + + + + + + + + + + + + + + + + {http://www.alfresco.org/model/content/1.0}content + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/alfresco/template-services-context.xml b/config/alfresco/template-services-context.xml index 7571db139b..82ab907b57 100644 --- a/config/alfresco/template-services-context.xml +++ b/config/alfresco/template-services-context.xml @@ -31,6 +31,15 @@ + + + xslt + + + xsl + + + diff --git a/source/java/org/alfresco/repo/rendition/RenditionServiceIntegrationTest.java b/source/java/org/alfresco/repo/rendition/RenditionServiceIntegrationTest.java index ebddb7325f..889f030b5c 100644 --- a/source/java/org/alfresco/repo/rendition/RenditionServiceIntegrationTest.java +++ b/source/java/org/alfresco/repo/rendition/RenditionServiceIntegrationTest.java @@ -235,6 +235,35 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest }); } + public void testRenderFreeMarkerTemplateOneTransaction() throws Exception + { + this.setComplete(); + this.endTransaction(); + final QName renditionName = QName.createQName(NamespaceService.RENDITION_MODEL_1_0_URI, + TemplatingRenderingEngine.NAME); + + this.renditionNode = transactionHelper + .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + // create test model + RenditionDefinition definition = renditionService.createRenditionDefinition(renditionName, + TemplatingRenderingEngine.NAME); + definition.setParameterValue(TemplatingRenderingEngine.PARAM_TEMPLATE_NODE, + nodeWithFreeMarkerContent); + ChildAssociationRef renditionAssoc = renditionService + .render(nodeWithDocContent, definition); + assertNotNull("The rendition association was null", renditionAssoc); + String output = readTextContent(renditionAssoc.getChildRef()); + assertNotNull("The rendition content was null.", output); + // check the output contains root node Id as expected. + assertTrue(output.contains(nodeWithDocContent.getId())); + return null; + } + }); + } + public void testRenderFreemarkerTemplatePath() throws Exception { //TODO displayName paths. diff --git a/source/java/org/alfresco/repo/rendition/executer/XSLTFunctions.java b/source/java/org/alfresco/repo/rendition/executer/XSLTFunctions.java new file mode 100644 index 0000000000..a9581ded93 --- /dev/null +++ b/source/java/org/alfresco/repo/rendition/executer/XSLTFunctions.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ + +package org.alfresco.repo.rendition.executer; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.XMLUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +/** + * @author Brian Remmington + * @since 3.3 + * + */ +public class XSLTFunctions +{ + private static final Log log = LogFactory.getLog(XSLTFunctions.class); + + private FileFolderService fileService; + private ContentService contentService; + private NodeService nodeService; + private NamespaceService namespaceService; + private DictionaryService dictionaryService; + + public XSLTFunctions() + { + } + + public Document parseXMLDocument(final NodeRef root, String repoPath) throws IOException, SAXException, + FileNotFoundException + { + String[] pathElements = breakDownPath(repoPath); + FileInfo file = fileService.resolveNamePath(root, Arrays.asList(pathElements)); + return XMLUtil.parse(file.getNodeRef(), contentService); + } + + public Map parseXMLDocuments(final String typeName, NodeRef rootNode, String repoPath) + throws IOException, SAXException + { + final Map result = new TreeMap(); + + String[] pathElements = breakDownPath(repoPath); + + try + { + FileInfo file = fileService.resolveNamePath(rootNode, Arrays.asList(pathElements)); + + if (file.isFolder()) + { + QName typeQName = QName.createQName(typeName, namespaceService); + Set types = new HashSet(dictionaryService.getSubTypes(typeQName, true)); + types.add(typeQName); + List children = nodeService.getChildAssocs(file.getNodeRef(), types); + for (ChildAssociationRef child : children) + { + String name = (String) nodeService.getProperty(child.getChildRef(), ContentModel.PROP_NAME); + Document doc = XMLUtil.parse(child.getChildRef(), contentService); + result.put(name, doc); + } + } + } + catch (Exception ex) + { + log.warn("Unexpected exception caught in call to parseXMLDocuments", ex); + } + return result; + } + + /** + * @param repoPath + * @return + */ + private String[] breakDownPath(String repoPath) + { + if (repoPath.startsWith("/")) + { + repoPath = repoPath.substring(1); + } + String[] pathElements = repoPath.split("/"); + return pathElements; + } + + /** + * Encodes invalid HTML characters. (Fix for ETWOTWO-504 issue) This code was adopted from WebDAVHelper.encodeHTML() + * method with some restrictions. + * + * @see press-release.xsl for pattern. + * + * @param text + * to encode + * @return encoded text + * @throws IOException + * @throws SAXException + */ + public String encodeQuotes(String text) throws IOException, SAXException + { + if (text == null) + { + return ""; + } + + StringBuilder sb = null; // create on demand + String enc; + char c; + for (int i = 0; i < text.length(); i++) + { + enc = null; + c = text.charAt(i); + switch (c) + { + case '"': + enc = """; + break; // " + // case '&': enc = "&"; break; //& + // case '<': enc = "<"; break; //< + // case '>': enc = ">"; break; //> + + // german umlauts + case '\u00E4': + enc = "ä"; + break; + case '\u00C4': + enc = "Ä"; + break; + case '\u00F6': + enc = "ö"; + break; + case '\u00D6': + enc = "Ö"; + break; + case '\u00FC': + enc = "ü"; + break; + case '\u00DC': + enc = "Ü"; + break; + case '\u00DF': + enc = "ß"; + break; + + // misc + // case 0x80: enc = "€"; break; sometimes euro symbol is ascii 128, should we suport it? + case '\u20AC': + enc = "€"; + break; + case '\u00AB': + enc = "«"; + break; + case '\u00BB': + enc = "»"; + break; + case '\u00A0': + enc = " "; + break; + + // case '': enc = "&trade"; break; + + default: + if (((int) c) >= 0x80) + { + // encode all non basic latin characters + enc = "&#" + ((int) c) + ";"; + } + break; + } + + if (enc != null) + { + if (sb == null) + { + String soFar = text.substring(0, i); + sb = new StringBuilder(i + 8); + sb.append(soFar); + } + sb.append(enc); + } + else + { + if (sb != null) + { + sb.append(c); + } + } + } + + if (sb == null) + { + return text; + } + else + { + return sb.toString(); + } + } + + /** + * @param fileService the fileService to set + */ + public void setFileService(FileFolderService fileService) + { + this.fileService = fileService; + } + + /** + * @param contentService the contentService to set + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * @param nodeService the nodeService to set + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param namespaceService the namespaceService to set + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param dictionaryService the dictionaryService to set + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } +} diff --git a/source/java/org/alfresco/repo/rendition/executer/XSLTFunctionsTest.java b/source/java/org/alfresco/repo/rendition/executer/XSLTFunctionsTest.java new file mode 100644 index 0000000000..89bb43c0c4 --- /dev/null +++ b/source/java/org/alfresco/repo/rendition/executer/XSLTFunctionsTest.java @@ -0,0 +1,397 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ + +package org.alfresco.repo.rendition.executer; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.model.filefolder.FileFolderServiceImpl; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.util.BaseAlfrescoSpringTest; +import org.alfresco.util.GUID; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + +/** + * @author Brian + * + */ +public class XSLTFunctionsTest extends BaseAlfrescoSpringTest +{ + private final static Log log = LogFactory.getLog(XSLTFunctionsTest.class); + private XSLTFunctions xsltFunctions; + private SearchService searchService; + private NodeRef companyHome; + private FileFolderService fileFolderService; + + /* (non-Javadoc) + * @see org.alfresco.util.BaseAlfrescoSpringTest#onSetUpInTransaction() + */ + @SuppressWarnings("deprecation") + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + this.searchService = (SearchService) this.applicationContext.getBean("SearchService"); + this.xsltFunctions = (XSLTFunctions) this.applicationContext.getBean("xsltFunctions"); + this.nodeService = (NodeService) this.applicationContext.getBean("NodeService"); + this.contentService = (ContentService) this.applicationContext.getBean("ContentService"); + this.fileFolderService = (FileFolderService) this.applicationContext.getBean("FileFolderService"); + ResultSet rs = searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, SearchService.LANGUAGE_XPATH, "/app:company_home"); + this.companyHome = rs.getNodeRef(0); + } + + public void testSimplestParseXMLDocument() + { + FileInfo file = createXmlFile(companyHome); + try + { + Document doc = xsltFunctions.parseXMLDocument(companyHome, file.getName()); + NodeList foodNodes = doc.getElementsByTagName("food"); + assertEquals(10, foodNodes.getLength()); + } + catch (Exception ex) + { + log.error("Error!", ex); + fail(ex.getMessage()); + } + } + + public void testPathParseXMLDocument() + { + String path = "path/to/xml/files"; + List pathElements = Arrays.asList(path.split("/")); + FileInfo folder = FileFolderServiceImpl.makeFolders(fileFolderService, companyHome, pathElements, ContentModel.TYPE_FOLDER); + FileInfo file = createXmlFile(folder.getNodeRef()); + + try + { + Document doc = xsltFunctions.parseXMLDocument(companyHome, path + "/" + file.getName()); + NodeList foodNodes = doc.getElementsByTagName("food"); + assertEquals(10, foodNodes.getLength()); + } + catch (Exception ex) + { + log.error("Error!", ex); + fail(ex.getMessage()); + } + } + + public void testParseXMLDocuments() + { + String path = "path/to/xml/files"; + List pathElements = Arrays.asList(path.split("/")); + FileInfo folder = FileFolderServiceImpl.makeFolders(fileFolderService, companyHome, pathElements, ContentModel.TYPE_FOLDER); + FileInfo file1 = createXmlFile(folder.getNodeRef()); + FileInfo file2 = createXmlFile(folder.getNodeRef()); + FileInfo file3 = createXmlFile(folder.getNodeRef()); + FileInfo file4 = createXmlFile(folder.getNodeRef()); + FileInfo file5 = createXmlFile(folder.getNodeRef()); + + try + { + Map xmlFileMap = xsltFunctions.parseXMLDocuments("cm:content", + companyHome, "/" + path); + assertEquals(5, xmlFileMap.size()); + Set names = new TreeSet(Arrays.asList(new String[] {file1.getName(), file2.getName(), + file3.getName(), file4.getName(), file5.getName()})); + names.removeAll(xmlFileMap.keySet()); + assertEquals(0, names.size()); + + NodeList foodNodes = xmlFileMap.get(file3.getName()).getElementsByTagName("food"); + assertEquals(10, foodNodes.getLength()); + } + catch (Exception ex) + { + log.error("Error!", ex); + fail(ex.getMessage()); + } + } + + /** + * + */ + private FileInfo createXmlFile(NodeRef folder) + { + String name = GUID.generate(); + FileInfo testXmlFile = fileFolderService.create(folder, name + ".xml", ContentModel.TYPE_CONTENT); + ContentWriter writer = contentService.getWriter(testXmlFile.getNodeRef(), ContentModel.PROP_CONTENT, true); + writer.setMimetype("text/xml"); + writer.setEncoding("UTF-8"); + writer.putContent(sampleXML); + return testXmlFile; + } + + + private String sampleXML = "" + + "" + + + "" + + "65" + + "20" + + "300" + + "2400" + + "300" + + "25" + + "50" + + "" + + + "" + + "Avocado Dip" + + "Sunnydale" + + "29" + + "" + + "11" + + "3" + + "5" + + "210" + + "2" + + "0" + + "1" + + "" + + "0" + + "0" + + "" + + "" + + "0" + + "0" + + "" + + "" + + + "" + + "Bagels, New York Style " + + "Thompson" + + "104" + + "" + + "4" + + "1" + + "0" + + "510" + + "54" + + "3" + + "11" + + "" + + "0" + + "0" + + "" + + "" + + "8" + + "20" + + "" + + "" + + + "" + + "Beef Frankfurter, Quarter Pound " + + "Armitage" + + "115" + + "" + + "32" + + "15" + + "65" + + "1100" + + "8" + + "0" + + "13" + + "" + + "0" + + "2" + + "" + + "" + + "1" + + "6" + + "" + + "" + + + "" + + "Chicken Pot Pie" + + "Lakeson" + + "198" + + "" + + "22" + + "9" + + "25" + + "810" + + "42" + + "2" + + "10" + + "" + + "20" + + "2" + + "" + + "" + + "2" + + "10" + + "" + + "" + + + "" + + "Cole Slaw" + + "Fresh Quick" + + "1.5" + + "" + + "0" + + "0" + + "0" + + "15" + + "5" + + "2" + + "1" + + "" + + "30" + + "45" + + "" + + "" + + "4" + + "2" + + "" + + "" + + + "" + + "Eggs" + + "Goodpath" + + "50" + + "" + + "4.5" + + "1.5" + + "215" + + "65" + + "1" + + "0" + + "6" + + "" + + "6" + + "0" + + "" + + "" + + "2" + + "4" + + "" + + "" + + + "" + + "Hazelnut Spread" + + "Ferreira" + + "2" + + "" + + "10" + + "2" + + "0" + + "20" + + "23" + + "2" + + "3" + + "" + + "0" + + "0" + + "" + + "" + + "6" + + "4" + + "" + + "" + + + "" + + "Potato Chips" + + "Lees" + + "28" + + "" + + "10" + + "3" + + "0" + + "180" + + "15" + + "1" + + "2" + + "" + + "0" + + "10" + + "" + + "" + + "0" + + "0" + + "" + + "" + + + "" + + "Soy Patties, Grilled" + + "Gardenproducts" + + "96" + + "" + + "5" + + "0" + + "0" + + "420" + + "10" + + "4" + + "9" + + "" + + "0" + + "0" + + "" + + "" + + "0" + + "0" + + "" + + "" + + + "" + + "Truffles, Dark Chocolate" + + "Lyndon's" + + "39" + + "" + + "19" + + "14" + + "25" + + "10" + + "16" + + "1" + + "1" + + "" + + "0" + + "0" + + "" + + "" + + "0" + + "0" + + "" + + "" + + + ""; +} diff --git a/source/java/org/alfresco/repo/rendition/executer/XSLTRenderingEngine.java b/source/java/org/alfresco/repo/rendition/executer/XSLTRenderingEngine.java new file mode 100644 index 0000000000..c633b2d27f --- /dev/null +++ b/source/java/org/alfresco/repo/rendition/executer/XSLTRenderingEngine.java @@ -0,0 +1,414 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ + +package org.alfresco.repo.rendition.executer; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Serializable; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.repo.template.TemplateProcessorMethod; +import org.alfresco.repo.template.XSLTProcessor; +import org.alfresco.repo.template.XSLTemplateModel; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.rendition.RenditionServiceException; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.TemplateService; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.XMLUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +/** + * @author Brian Remmington + * @since 3.3 + */ +public class XSLTRenderingEngine extends AbstractRenderingEngine +{ + private static final Log log = LogFactory.getLog(XSLTRenderingEngine.class); + + public static final String NAME = "xsltRenderingEngine"; + public static final String PARAM_MODEL = "model"; + public static final String PARAM_TEMPLATE = "template_string"; + public static final String PARAM_TEMPLATE_NODE = "template_node"; + public static final String PARAM_TEMPLATE_PATH = "template_path"; + + private TemplateService templateService; + private XSLTFunctions xsltFunctions; + private NamespacePrefixResolver namespacePrefixResolver; + private FileFolderService fileFolderService; + private SearchService searchService; + + /* + * @see org.alfresco.repo.rendition.executer.AbstractRenderingEngine#render(org + * .alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.rendition.RenditionDefinition, + * org.alfresco.service.cmr.repository.ContentReader, org.alfresco.service.cmr.repository.ChildAssociationRef) + */ + @SuppressWarnings("unchecked") + @Override + protected void render(RenderingContext context) + { + NodeRef templateNode = getTemplateNode(context); + Map paramMap = context.getCheckedParam(PARAM_MODEL, Map.class); + try + { + XSLTemplateModel model = buildModel(context, paramMap); + ContentWriter contentWriter = context.makeContentWriter(); + Writer writer = new OutputStreamWriter(contentWriter.getContentOutputStream()); + processTemplate(context, templateNode, model, writer); + writer.flush(); + writer.close(); + } + catch (Exception ex) + { + log.warn("Unexpected error while rendering through XSLT rendering engine.", ex); + } + } + + private void processTemplate(RenderingContext context, NodeRef templateNode, XSLTemplateModel model, Writer out) + { + String template = context.getCheckedParam(PARAM_TEMPLATE, String.class); + if (template != null) + { + templateService.processTemplateString("xslt", (String) template, model, out); + } + else if (templateNode != null) + { + templateService.processTemplate("xslt", templateNode.toString(), model, out); + } + else + { + throwTemplateParamsNotFoundException(); + } + } + + private void throwTemplateParamsNotFoundException() + { + StringBuilder msg = new StringBuilder("This action requires that either the "); + msg.append(PARAM_TEMPLATE); + msg.append(" parameter or the "); + msg.append(PARAM_TEMPLATE_NODE); + msg.append(" parameter be specified. "); + throw new RenditionServiceException(msg.toString()); + } + + private NodeRef getTemplateNode(RenderingContext context) + { + NodeRef node = context.getCheckedParam(PARAM_TEMPLATE_NODE, NodeRef.class); + if (node == null) + { + String path = context.getCheckedParam(PARAM_TEMPLATE_PATH, String.class); + if (path != null && path.length() > 0) + { + StoreRef storeRef = context.getDestinationNode().getStoreRef(); + ResultSet result = searchService.query(storeRef, SearchService.LANGUAGE_XPATH, path); + if (result.length() != 1) + { + throw new RenditionServiceException("Could not find template node for path: " + path); + } + node = result.getNodeRef(0); + } + } + return node; + } + + @SuppressWarnings("serial") + protected XSLTemplateModel buildModel(RenderingContext context, Map suppliedParams) + throws IOException, SAXException + { + final NodeRef sourceNode = context.getSourceNode(); + final NodeRef parentNode = nodeService.getPrimaryParent(context.getSourceNode()).getParentRef(); + final String sourcePath = getPath(sourceNode); + final String parentPath = getPath(parentNode); + + XSLTemplateModel model = new XSLTemplateModel(); + + // add simple scalar parameters + model.put(QName.createQName(NamespaceService.ALFRESCO_URI, "date"), new Date()); + + model.put(QName.createQName(NamespaceService.ALFRESCO_PREFIX, "source_file_name", namespacePrefixResolver), + nodeService.getProperty(context.getSourceNode(), ContentModel.PROP_NAME)); + model.put(QName.createQName(NamespaceService.ALFRESCO_PREFIX, "source_path", namespacePrefixResolver), + sourcePath); + model.put(QName.createQName(NamespaceService.ALFRESCO_PREFIX, "parent_path", namespacePrefixResolver), + parentPath); + + // add methods + model.put(QName.createQName(NamespaceService.ALFRESCO_PREFIX, "encodeQuotes", namespacePrefixResolver), + new TemplateProcessorMethod() + { + public Object exec(final Object[] arguments) throws IOException, SAXException + { + if (arguments.length != 1) + { + throw new IllegalArgumentException("expected 1 argument to encodeQuotes. got " + + arguments.length); + + } + if (!(arguments[0] instanceof String)) + { + throw new ClassCastException("expected arguments[0] to be a " + String.class.getName() + + ". got a " + arguments[0].getClass().getName() + "."); + } + String text = (String) arguments[0]; + + if (log.isDebugEnabled()) + { + log.debug("tpm_encodeQuotes('" + text + "'), parentPath = " + parentPath); + } + + final String result = xsltFunctions.encodeQuotes(text); + return result; + } + }); + + model.put(QName.createQName(NamespaceService.ALFRESCO_PREFIX, "parseXMLDocument", namespacePrefixResolver), + new TemplateProcessorMethod() + { + public Object exec(final Object[] arguments) throws IOException, SAXException + { + if (arguments.length != 1) + { + throw new IllegalArgumentException("expected 1 argument to parseXMLDocument. got " + + arguments.length); + + } + if (!(arguments[0] instanceof String)) + { + throw new ClassCastException("expected arguments[0] to be a " + String.class.getName() + + ". got a " + arguments[0].getClass().getName() + "."); + } + String path = (String) arguments[0]; + + if (log.isDebugEnabled()) + { + log.debug("parseXMLDocument('" + path + "'), parentPath = " + parentPath); + } + + Document d = null; + try + { + d = xsltFunctions.parseXMLDocument(parentNode, path); + } + catch (Exception ex) + { + + } + return d == null ? null : d.getDocumentElement(); + } + }); + model.put(QName.createQName(NamespaceService.ALFRESCO_PREFIX, "parseXMLDocuments", namespacePrefixResolver), + new TemplateProcessorMethod() + { + public Object exec(final Object[] arguments) throws IOException, SAXException + { + if (arguments.length > 2) + { + throw new IllegalArgumentException("expected one or two arguments to " + + "parseXMLDocuments. got " + arguments.length); + } + if (!(arguments[0] instanceof String)) + { + throw new ClassCastException("expected arguments[0] to be a " + String.class.getName() + + ". got a " + arguments[0].getClass().getName() + "."); + } + + if (arguments.length == 2 && !(arguments[1] instanceof String)) + { + throw new ClassCastException("expected arguments[1] to be a " + String.class.getName() + + ". got a " + arguments[1].getClass().getName() + "."); + } + + String path = arguments.length == 2 ? (String) arguments[1] : ""; + final String typeName = (String) arguments[0]; + + if (log.isDebugEnabled()) + { + log.debug("tpm_parseXMLDocuments('" + typeName + "','" + path + "'), parentPath = " + + parentPath); + } + + final Map resultMap = xsltFunctions.parseXMLDocuments(typeName, parentNode, + path); + + if (log.isDebugEnabled()) + { + log.debug("received " + resultMap.size() + " documents in " + path + " with form name " + + typeName); + } + + // create a root document for rooting all the results. we do this + // so that each document root element has a common parent node + // and so that xpath axes work properly + final Document rootNodeDocument = XMLUtil.newDocument(); + final Element rootNodeDocumentEl = rootNodeDocument.createElementNS( + NamespaceService.ALFRESCO_URI, NamespaceService.ALFRESCO_PREFIX + ":file_list"); + rootNodeDocumentEl.setAttribute("xmlns:" + NamespaceService.ALFRESCO_PREFIX, + NamespaceService.ALFRESCO_URI); + rootNodeDocument.appendChild(rootNodeDocumentEl); + + final List result = new ArrayList(resultMap.size()); + for (Map.Entry e : resultMap.entrySet()) + { + final Element documentEl = e.getValue().getDocumentElement(); + documentEl.setAttribute("xmlns:" + NamespaceService.ALFRESCO_PREFIX, + NamespaceService.ALFRESCO_URI); + documentEl.setAttributeNS(NamespaceService.ALFRESCO_URI, NamespaceService.ALFRESCO_PREFIX + + ":file_name", e.getKey()); + final Node n = rootNodeDocument.importNode(documentEl, true); + rootNodeDocumentEl.appendChild(n); + result.add(n); + } + return result.toArray(new Node[result.size()]); + } + }); + + if (suppliedParams != null) + { + for (Map.Entry suppliedParam : suppliedParams.entrySet()) + { + model.put(QName.createQName(suppliedParam.getKey()), suppliedParam.getValue()); + } + } + + // add the xml document + model.put(XSLTProcessor.ROOT_NAMESPACE, XMLUtil.parse(sourceNode, contentService)); + return model; + } + + /** + * @param nodeRef + * @return + * @throws FileNotFoundException + */ + private String getPath(NodeRef nodeRef) + { + StringBuilder sb = new StringBuilder(); + try + { + List parentFileInfoList = fileFolderService.getNamePath(null, nodeRef); + for (FileInfo fileInfo : parentFileInfoList) + { + sb.append('/'); + sb.append(fileInfo.getName()); + } + } + catch (FileNotFoundException ex) + { + log.info("Unexpected problem: error while calculating path to node " + nodeRef, ex); + } + String path = sb.toString(); + return path; + } + + /* + * @seeorg.alfresco.repo.rendition.executer.AbstractRenderingEngine# getParameterDefinitions() + */ + @Override + protected Collection getParameterDefinitions() + { + Collection paramList = super.getParameterDefinitions(); + ParameterDefinitionImpl modelParamDef = new ParameterDefinitionImpl(PARAM_MODEL, DataTypeDefinition.ANY, false, + getParamDisplayLabel(PARAM_MODEL)); + ParameterDefinitionImpl templateParamDef = new ParameterDefinitionImpl(// + PARAM_TEMPLATE, DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_TEMPLATE)); + ParameterDefinitionImpl templateNodeParamDef = new ParameterDefinitionImpl(PARAM_TEMPLATE_NODE, + DataTypeDefinition.NODE_REF, false, getParamDisplayLabel(PARAM_TEMPLATE_NODE)); + ParameterDefinitionImpl templatePathParamDef = new ParameterDefinitionImpl(PARAM_TEMPLATE_PATH, + DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_TEMPLATE_PATH)); + paramList.add(modelParamDef); + paramList.add(templateParamDef); + paramList.add(templateNodeParamDef); + paramList.add(templatePathParamDef); + return paramList; + } + + /** + * @param templateService + * the templateService to set + */ + public void setTemplateService(TemplateService templateService) + { + this.templateService = templateService; + } + + /** + * @param xsltFunctions + * the xsltFunctions to set + */ + public void setXsltFunctions(XSLTFunctions xsltFunctions) + { + this.xsltFunctions = xsltFunctions; + } + + /** + * @param namespacePrefixResolver + * the namespacePrefixResolver to set + */ + public void setNamespacePrefixResolver(NamespacePrefixResolver namespacePrefixResolver) + { + this.namespacePrefixResolver = namespacePrefixResolver; + } + + /** + * @param fileFolderService + * the fileFolderService to set + */ + public void setFileFolderService(FileFolderService fileFolderService) + { + this.fileFolderService = fileFolderService; + } + + /** + * @param searchService + * the searchService to set + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } +} diff --git a/source/java/org/alfresco/repo/rendition/executer/XSLTRenderingEngineTest.java b/source/java/org/alfresco/repo/rendition/executer/XSLTRenderingEngineTest.java new file mode 100644 index 0000000000..afb01c25fa --- /dev/null +++ b/source/java/org/alfresco/repo/rendition/executer/XSLTRenderingEngineTest.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ + +package org.alfresco.repo.rendition.executer; + +import java.util.Arrays; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.model.filefolder.FileFolderServiceImpl; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.rendition.RenditionDefinition; +import org.alfresco.service.cmr.rendition.RenditionService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.TemplateProcessor; +import org.alfresco.service.cmr.repository.TemplateService; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseAlfrescoSpringTest; +import org.alfresco.util.GUID; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @author Brian + * + */ +public class XSLTRenderingEngineTest extends BaseAlfrescoSpringTest +{ + private final static Log log = LogFactory.getLog(XSLTRenderingEngineTest.class); + private XSLTFunctions xsltFunctions; + private SearchService searchService; + private NodeRef companyHome; + private FileFolderService fileFolderService; + private TemplateProcessor xsltProcessor; + private TemplateService templateService; + private RenditionService renditionService; + + /* + * (non-Javadoc) + * + * @see org.alfresco.util.BaseAlfrescoSpringTest#onSetUpInTransaction() + */ + @SuppressWarnings("deprecation") + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + this.searchService = (SearchService) this.applicationContext.getBean("SearchService"); + this.xsltFunctions = (XSLTFunctions) this.applicationContext.getBean("xsltFunctions"); + this.nodeService = (NodeService) this.applicationContext.getBean("NodeService"); + this.contentService = (ContentService) this.applicationContext.getBean("ContentService"); + this.fileFolderService = (FileFolderService) this.applicationContext.getBean("FileFolderService"); + this.xsltProcessor = (TemplateProcessor) this.applicationContext.getBean("xsltProcessor"); + this.templateService = (TemplateService) this.applicationContext.getBean("TemplateService"); + this.renditionService = (RenditionService) this.applicationContext.getBean("RenditionService"); + ResultSet rs = searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, SearchService.LANGUAGE_XPATH, + "/app:company_home"); + this.companyHome = rs.getNodeRef(0); + } + + public void testSimplestStringTemplate() throws Exception + { + try + { + FileInfo file = createXmlFile(companyHome); + FileInfo xslFile = createXmlFile(companyHome, verySimpleXSLT); + + RenditionDefinition def = renditionService.createRenditionDefinition(QName.createQName("Test"), XSLTRenderingEngine.NAME); + def.setParameterValue(XSLTRenderingEngine.PARAM_TEMPLATE_NODE, xslFile.getNodeRef()); + + ChildAssociationRef rendition = renditionService.render(file.getNodeRef(), def); + + assertNotNull(rendition); + + ContentReader reader = contentService.getReader(rendition.getChildRef(), ContentModel.PROP_CONTENT); + assertNotNull(reader); + String output = reader.getContentString(); + + log.debug("XSLT Processor output: " + output); + assertEquals("Avocado DipBagels, New York StyleBeef Frankfurter, Quarter PoundChicken Pot PieCole SlawEggsHazelnut SpreadPotato ChipsSoy Patties, GrilledTruffles, Dark Chocolate", output); + } + catch (Exception ex) + { + log.error("Error!", ex); + fail(); + } + } + + + public void testParseXMLDocument() throws Exception + { + try + { + FileInfo file = createXmlFile(companyHome); + createXmlFile(companyHome, "TestXML.xml", sampleXML); + FileInfo xslFile = createXmlFile(companyHome, callParseXmlDocument); + + RenditionDefinition def = renditionService.createRenditionDefinition(QName.createQName("Test"), XSLTRenderingEngine.NAME); + def.setParameterValue(XSLTRenderingEngine.PARAM_TEMPLATE_NODE, xslFile.getNodeRef()); + + ChildAssociationRef rendition = renditionService.render(file.getNodeRef(), def); + + assertNotNull(rendition); + + ContentReader reader = contentService.getReader(rendition.getChildRef(), ContentModel.PROP_CONTENT); + assertNotNull(reader); + String output = reader.getContentString(); + + log.debug("XSLT Processor output: " + output); + assertEquals("Avocado DipBagels, New York StyleBeef Frankfurter, Quarter PoundChicken Pot PieCole SlawEggsHazelnut SpreadPotato ChipsSoy Patties, GrilledTruffles, Dark Chocolate", output); + } + catch (Exception ex) + { + log.error("Error!", ex); + fail(); + } + } + + public void testParseXMLDocuments() throws Exception + { + try + { + FileInfo file = createXmlFile(companyHome); + String path = "path/to/xml/files"; + List pathElements = Arrays.asList(path.split("/")); + FileInfo folder = FileFolderServiceImpl.makeFolders(fileFolderService, companyHome, pathElements, ContentModel.TYPE_FOLDER); + createXmlFile(folder.getNodeRef()); + createXmlFile(folder.getNodeRef()); + createXmlFile(folder.getNodeRef()); + createXmlFile(folder.getNodeRef()); + createXmlFile(folder.getNodeRef()); + FileInfo xslFile = createXmlFile(companyHome, callParseXmlDocuments); + + RenditionDefinition def = renditionService.createRenditionDefinition(QName.createQName("Test"), XSLTRenderingEngine.NAME); + def.setParameterValue(XSLTRenderingEngine.PARAM_TEMPLATE_NODE, xslFile.getNodeRef()); + + ChildAssociationRef rendition = renditionService.render(file.getNodeRef(), def); + + assertNotNull(rendition); + + ContentReader reader = contentService.getReader(rendition.getChildRef(), ContentModel.PROP_CONTENT); + assertNotNull(reader); + String output = reader.getContentString(); + + log.debug("XSLT Processor output: " + output); + assertEquals( + "Avocado DipBagels, New York StyleBeef Frankfurter, Quarter PoundChicken Pot PieCole SlawEggsHazelnut SpreadPotato ChipsSoy Patties, GrilledTruffles, Dark Chocolate"+ + "Avocado DipBagels, New York StyleBeef Frankfurter, Quarter PoundChicken Pot PieCole SlawEggsHazelnut SpreadPotato ChipsSoy Patties, GrilledTruffles, Dark Chocolate"+ + "Avocado DipBagels, New York StyleBeef Frankfurter, Quarter PoundChicken Pot PieCole SlawEggsHazelnut SpreadPotato ChipsSoy Patties, GrilledTruffles, Dark Chocolate"+ + "Avocado DipBagels, New York StyleBeef Frankfurter, Quarter PoundChicken Pot PieCole SlawEggsHazelnut SpreadPotato ChipsSoy Patties, GrilledTruffles, Dark Chocolate"+ + "Avocado DipBagels, New York StyleBeef Frankfurter, Quarter PoundChicken Pot PieCole SlawEggsHazelnut SpreadPotato ChipsSoy Patties, GrilledTruffles, Dark Chocolate" + , output); + } + catch (Exception ex) + { + log.error("Error!", ex); + fail(); + } + } + + + private FileInfo createXmlFile(NodeRef folder) + { + return createXmlFile(folder, sampleXML); + } + + private FileInfo createXmlFile(NodeRef folder, String content) + { + String name = GUID.generate() + ".xml"; + return createXmlFile(folder, name, content); + } + + private FileInfo createXmlFile(NodeRef folder, String filename, String content) + { + FileInfo testXmlFile = fileFolderService.create(folder, filename, ContentModel.TYPE_CONTENT); + ContentWriter writer = contentService.getWriter(testXmlFile.getNodeRef(), ContentModel.PROP_CONTENT, true); + writer.setMimetype("text/xml"); + writer.setEncoding("UTF-8"); + writer.putContent(content); + return testXmlFile; + } + + private String sampleXML = "" + "" + + + "" + "65" + "20" + + "300" + "2400" + + "300" + "25" + "50" + + "" + + + "" + "Avocado Dip" + "Sunnydale" + "29" + + "" + "11" + + "3" + "5" + "210" + + "2" + "0" + "1" + "" + "0" + "0" + + "" + "" + "0" + "0" + "" + "" + + + "" + "Bagels, New York Style" + "Thompson" + + "104" + "" + + "4" + "1" + "0" + + "510" + "54" + "3" + "11" + "" + + "0" + "0" + "" + "" + "8" + "20" + "" + + "" + + + "" + "Beef Frankfurter, Quarter Pound" + "Armitage" + + "115" + "" + + "32" + "15" + "65" + + "1100" + "8" + "0" + "13" + "" + + "0" + "2" + "" + "" + "1" + "6" + "" + + "" + + + "" + "Chicken Pot Pie" + "Lakeson" + "198" + + "" + "22" + + "9" + "25" + "810" + + "42" + "2" + "10" + "" + "20" + + "2" + "" + "" + "2" + "10" + "" + "" + + + "" + "Cole Slaw" + "Fresh Quick" + "1.5" + + "" + "0" + "0" + + "0" + "15" + "5" + "2" + + "1" + "" + "30" + "45" + "" + "" + + "4" + "2" + "" + "" + + + "" + "Eggs" + "Goodpath" + "50" + + "" + "4.5" + + "1.5" + "215" + "65" + + "1" + "0" + "6" + "" + "6" + "0" + + "" + "" + "2" + "4" + "" + "" + + + "" + "Hazelnut Spread" + "Ferreira" + "2" + + "" + "10" + "2" + + "0" + "20" + "23" + "2" + + "3" + "" + "0" + "0" + "" + "" + + "6" + "4" + "" + "" + + + "" + "Potato Chips" + "Lees" + "28" + + "" + "10" + "3" + + "0" + "180" + "15" + "1" + + "2" + "" + "0" + "10" + "" + "" + + "0" + "0" + "" + "" + + + "" + "Soy Patties, Grilled" + "Gardenproducts" + + "96" + "" + "5" + + "0" + "0" + "420" + + "10" + "4" + "9" + "" + "0" + "0" + + "" + "" + "0" + "0" + "" + "" + + + "" + "Truffles, Dark Chocolate" + "Lyndon's" + + "39" + "" + + "19" + "14" + "25" + + "10" + "16" + "1" + "1" + "" + + "0" + "0" + "" + "" + "0" + "0" + "" + + "" + + + ""; + + + private String verySimpleXSLT = "" + " " + "" + + + "" + + + "" + "" + + "" + "" + "" + ""; + + private String callParseXmlDocument = "" + " " + "" + + + "" + + + "" + + "" + "" + + "" + "" + "" + ""; + + private String callParseXmlDocuments = "" + " " + "" + + + "" + + + "" + + "" + "" + "" + + "" + "" + "" + "" + ""; + + +} diff --git a/source/java/org/alfresco/repo/template/ClassPathRepoTemplateLoader.java b/source/java/org/alfresco/repo/template/ClassPathRepoTemplateLoader.java index b9d57e5245..766746e06a 100644 --- a/source/java/org/alfresco/repo/template/ClassPathRepoTemplateLoader.java +++ b/source/java/org/alfresco/repo/template/ClassPathRepoTemplateLoader.java @@ -26,13 +26,11 @@ import java.net.URL; import java.net.URLConnection; import org.alfresco.model.ContentModel; -import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.StoreRef; -import org.alfresco.util.ApplicationContextHelper; import freemarker.cache.TemplateLoader; @@ -85,8 +83,14 @@ public class ClassPathRepoTemplateLoader implements TemplateLoader } else { - URL url = this.getClass().getClassLoader().getResource(name); - return url == null ? null : new ClassPathTemplateSource(url, encoding); + //Fix a common problem: classpath resource paths should not start with "/" + if (name.startsWith("/")) + { + name = name.substring(1); + } + ClassLoader classLoader = getClass().getClassLoader(); + URL url = classLoader.getResource(name); + return url == null ? null : new ClassPathTemplateSource(classLoader, name, encoding); } } @@ -116,13 +120,20 @@ public class ClassPathRepoTemplateLoader implements TemplateLoader /** * Class used as a base for custom Template Source objects */ - abstract class BaseTemplateSource + abstract class BaseTemplateSource implements TemplateSource { public abstract Reader getReader(String encoding) throws IOException; public abstract void close() throws IOException; public abstract long lastModified(); + + public InputStream getResource(String name) + { + return getRelativeResource(name); + } + + protected abstract InputStream getRelativeResource(String name); } @@ -135,10 +146,14 @@ public class ClassPathRepoTemplateLoader implements TemplateLoader private URLConnection conn; private InputStream inputStream; private String encoding; + private ClassLoader classLoader; + private String resourceName; - ClassPathTemplateSource(URL url, String encoding) throws IOException + ClassPathTemplateSource(ClassLoader classLoader, String name, String encoding) throws IOException { - this.url = url; + this.classLoader = classLoader; + this.resourceName = name; + this.url = classLoader.getResource(name); this.conn = url.openConnection(); this.encoding = encoding; } @@ -198,6 +213,32 @@ public class ClassPathRepoTemplateLoader implements TemplateLoader conn = null; } } + + /* (non-Javadoc) + * @see org.alfresco.repo.template.ClassPathRepoTemplateLoader.BaseTemplateSource#getRelativeResource(java.lang.String) + */ + @Override + protected InputStream getRelativeResource(String name) + { + String newResourceName = name; + if (!name.startsWith("/")) + { + int lastSlash = resourceName.lastIndexOf('/'); + if (lastSlash != -1) + { + newResourceName = name.substring(0, lastSlash) + "/" + name; + } + } + URL url = classLoader.getResource(newResourceName); + try + { + return (url == null) ? null : url.openConnection().getInputStream(); + } + catch (IOException e) + { + return null; + } + } } /** @@ -263,5 +304,25 @@ public class ClassPathRepoTemplateLoader implements TemplateLoader conn = null; } } + + /* (non-Javadoc) + * @see org.alfresco.repo.template.ClassPathRepoTemplateLoader.BaseTemplateSource#getRelativeResource(java.lang.String) + */ + @Override + protected InputStream getRelativeResource(String name) + { + InputStream stream = null; + NodeRef parentRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); + NodeRef child = nodeService.getChildByName(parentRef, ContentModel.ASSOC_CONTAINS, name); + if (child != null) + { + ContentReader contentReader = contentService.getReader(child, ContentModel.PROP_CONTENT); + if (contentReader.exists()) + { + stream = contentReader.getContentInputStream(); + } + } + return stream; + } } } diff --git a/source/java/org/alfresco/repo/template/TemplateProcessorMethod.java b/source/java/org/alfresco/repo/template/TemplateProcessorMethod.java new file mode 100644 index 0000000000..03dcf3e01e --- /dev/null +++ b/source/java/org/alfresco/repo/template/TemplateProcessorMethod.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco 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, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Alfresco. If not, see . + */ + +package org.alfresco.repo.template; + +import java.io.Serializable; + +/** + * @author Brian + * + */ +public interface TemplateProcessorMethod extends Serializable +{ + Object exec(final Object[] arguments) throws Exception; +} diff --git a/source/java/org/alfresco/repo/template/TemplateSource.java b/source/java/org/alfresco/repo/template/TemplateSource.java new file mode 100644 index 0000000000..3b7baf0755 --- /dev/null +++ b/source/java/org/alfresco/repo/template/TemplateSource.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco 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, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Alfresco. If not, see . + */ + +package org.alfresco.repo.template; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +public interface TemplateSource +{ + Reader getReader(String encoding) throws IOException; + + void close() throws IOException; + + long lastModified(); + + InputStream getResource(String name); +} diff --git a/source/java/org/alfresco/repo/template/XSLTProcessor.java b/source/java/org/alfresco/repo/template/XSLTProcessor.java new file mode 100644 index 0000000000..e988073d36 --- /dev/null +++ b/source/java/org/alfresco/repo/template/XSLTProcessor.java @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco 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, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Alfresco. If not, see . + */ +package org.alfresco.repo.template; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.xml.transform.ErrorListener; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.URIResolver; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.alfresco.repo.processor.BaseProcessor; +import org.alfresco.service.cmr.repository.TemplateException; +import org.alfresco.service.cmr.repository.TemplateProcessor; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.XMLUtil; +import org.apache.bsf.BSFManager; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.xml.utils.Constants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import freemarker.cache.TemplateLoader; + +public class XSLTProcessor extends BaseProcessor implements TemplateProcessor +{ + private static final Log log = LogFactory.getLog(XSLTProcessor.class); + + private static final String MSG_UNABLE_TO_READ_TEMPLATE = "template.xslt.read_error"; + private static final String MSG_UNABLE_TO_PARSE_TEMPLATE = "template.xslt.parse_error"; + + public final static QName ROOT_NAMESPACE = QName.createQName(null, "root_namespace"); + + private String defaultEncoding = "UTF-8"; + private TemplateLoader templateLoader; + + public void register() + { + super.register(); + templateLoader = new ClassPathRepoTemplateLoader(this.services.getNodeService(), this.services + .getContentService(), defaultEncoding); + } + + public void process(String template, Object model, Writer out) + { + TemplateSource templateSource; + try + { + templateSource = (TemplateSource) templateLoader.findTemplateSource(template); + } + catch (IOException ex) + { + throw new TemplateException(MSG_UNABLE_TO_READ_TEMPLATE, new Object[] { ex.getMessage() }, ex); + } + process(templateSource, model, out); + } + + public void processString(final String template, Object model, Writer out) + { + TemplateSource stringTemplateSource = new TemplateSource() + { + public long lastModified() + { + return System.currentTimeMillis(); + } + + public InputStream getResource(String name) + { + return null; + } + + public Reader getReader(String encoding) throws IOException + { + return new StringReader(template); + } + + public void close() throws IOException + { + } + }; + process(stringTemplateSource, model, out); + } + + /** + * @param templateSource + * @param xsltModel + * @param out + */ + private void process(TemplateSource templateSource, Object model, Writer out) + { + if ((model == null) || !XSLTemplateModel.class.isAssignableFrom(model.getClass())) + { + throw new IllegalArgumentException("\"model\" must be an XSLTemplateModel object: " + model); + } + + XSLTemplateModel xsltModel = (XSLTemplateModel) model; + System.setProperty("org.apache.xalan.extensions.bsf.BSFManager", BSFManager.class.getName()); + + Document xslTemplate; + try + { + xslTemplate = XMLUtil.parse(templateSource.getReader(defaultEncoding)); + } + catch (IOException ex) + { + throw new TemplateException(MSG_UNABLE_TO_READ_TEMPLATE, new Object[] { ex.getMessage() }, ex); + } + catch (SAXException sax) + { + throw new TemplateException(MSG_UNABLE_TO_PARSE_TEMPLATE, new Object[] { sax.getMessage() }, sax); + } + finally + { + try + { + templateSource.close(); + } + catch (IOException ex) + { + // There's little to be done here. Log it and carry on + log.warn("Error while trying to close template stream", ex); + } + } + + List scriptIds = addScripts(xsltModel, xslTemplate); + addParameters(xsltModel, xslTemplate); + + final LinkedList errors = new LinkedList(); + final ErrorListener errorListener = new ErrorListener() + { + public void error(final TransformerException te) throws TransformerException + { + log.debug("error " + te.getMessageAndLocation()); + errors.add(te); + } + + public void fatalError(final TransformerException te) throws TransformerException + { + log.debug("fatalError " + te.getMessageAndLocation()); + throw te; + } + + public void warning(final TransformerException te) throws TransformerException + { + log.debug("warning " + te.getMessageAndLocation()); + errors.add(te); + } + }; + + final TemplateSource resourceSource = templateSource; + final URIResolver uriResolver = new URIResolver() + { + public Source resolve(final String href, String base) throws TransformerException + { + if (log.isDebugEnabled()) + { + log.debug("request to resolve href " + href + " using base " + base); + } + InputStream in = null; + try + { + in = resourceSource.getResource(href); + if (in == null) + { + throw new TransformerException("unable to resolve href " + href); + } + + Document d = XMLUtil.parse(in); + if (log.isDebugEnabled()) + { + log.debug("loaded " + XMLUtil.toString(d)); + } + return new DOMSource(d); + } + catch (TransformerException ex) + { + throw ex; + } + catch (Exception e) + { + throw new TransformerException("unable to load " + href, e); + } + } + }; + + Source xmlSource = this.getXMLSource(xsltModel); + + Transformer t = null; + try + { + final TransformerFactory tf = TransformerFactory.newInstance(); + tf.setErrorListener(errorListener); + tf.setURIResolver(uriResolver); + + if (log.isDebugEnabled()) + { + log.debug("xslTemplate: \n" + XMLUtil.toString(xslTemplate)); + } + + t = tf.newTransformer(new DOMSource(xslTemplate)); + + if (errors.size() != 0) + { + final StringBuilder msg = new StringBuilder("errors encountered creating tranformer ... \n"); + for (TransformerException te : errors) + { + msg.append(te.getMessageAndLocation()).append("\n"); + } + throw new TemplateException(msg.toString()); + } + + t.setErrorListener(errorListener); + t.setURIResolver(uriResolver); + t.setParameter("versionParam", "2.0"); + } + catch (TransformerConfigurationException tce) + { + log.error(tce); + throw new TemplateException(tce.getMessage(), tce); + } + + try + { + t.transform(xmlSource, new StreamResult(out)); + } + catch (TransformerException te) + { + log.error(te.getMessageAndLocation()); + throw new TemplateException(te.getMessageAndLocation(), te); + } + catch (Exception e) + { + log.error("unexpected error " + e); + throw new TemplateException(e.getMessage(), e); + } + finally + { + //Clear out any scripts that were created for this transform + if (!scriptIds.isEmpty()) + { + XSLTProcessorMethodInvoker.removeMethods(scriptIds); + } + } + + if (errors.size() != 0) + { + final StringBuilder msg = new StringBuilder("errors encountered during transformation ... \n"); + for (TransformerException te : errors) + { + msg.append(te.getMessageAndLocation()).append("\n"); + } + throw new TemplateException(msg.toString()); + } + } + + /** + * Adds a script element to the xsl which makes static methods on this object available to the xsl tempalte. + * + * @param xslTemplate + * the xsl template + */ + protected List addScripts(final XSLTemplateModel xsltModel, final Document xslTemplate) + { + final Map>> methods = new HashMap>>(); + for (final Map.Entry entry : xsltModel.entrySet()) + { + if (entry.getValue() instanceof TemplateProcessorMethod) + { + final String prefix = QName.splitPrefixedQName(entry.getKey().toPrefixString())[0]; + final QName qn = QName.createQName(entry.getKey().getNamespaceURI(), prefix); + if (!methods.containsKey(qn)) + { + methods.put(qn, new LinkedList>()); + } + methods.get(qn).add(entry); + } + } + + final Element docEl = xslTemplate.getDocumentElement(); + final String XALAN_NS = Constants.S_BUILTIN_EXTENSIONS_URL; + final String XALAN_NS_PREFIX = "xalan"; + docEl.setAttribute("xmlns:" + XALAN_NS_PREFIX, XALAN_NS); + + final Set excludePrefixes = new HashSet(); + if (docEl.hasAttribute("exclude-result-prefixes")) + { + excludePrefixes.addAll(Arrays.asList(docEl.getAttribute("exclude-result-prefixes").split(" "))); + } + excludePrefixes.add(XALAN_NS_PREFIX); + + final List result = new LinkedList(); + for (QName ns : methods.keySet()) + { + final String prefix = ns.getLocalName(); + docEl.setAttribute("xmlns:" + prefix, ns.getNamespaceURI()); + excludePrefixes.add(prefix); + + final Element compEl = xslTemplate.createElementNS(XALAN_NS, XALAN_NS_PREFIX + ":component"); + compEl.setAttribute("prefix", prefix); + docEl.appendChild(compEl); + String functionNames = null; + final Element scriptEl = xslTemplate.createElementNS(XALAN_NS, XALAN_NS_PREFIX + ":script"); + scriptEl.setAttribute("lang", "javascript"); + final StringBuilder js = new StringBuilder("var _xsltp_invoke = java.lang.Class.forName('" + + XSLTProcessorMethodInvoker.class.getName() + "').newInstance();\n" + + "function _xsltp_to_java_array(js_array) {\n" + + "var java_array = java.lang.reflect.Array.newInstance(java.lang.Object, js_array.length);\n" + + "for (var i = 0; i < js_array.length; i++) { java_array[i] = js_array[i]; }\n" + + "return java_array; }\n"); + for (final Map.Entry entry : methods.get(ns)) + { + if (functionNames == null) + { + functionNames = entry.getKey().getLocalName(); + } + else + { + functionNames += " " + entry.getKey().getLocalName(); + } + final String id = entry.getKey().getLocalName() + entry.getValue().hashCode(); + js.append("function " + entry.getKey().getLocalName() + "() { return _xsltp_invoke.invokeMethod('" + id + + "', _xsltp_to_java_array(arguments)); }\n"); + XSLTProcessorMethodInvoker.addMethod(id, (TemplateProcessorMethod) entry.getValue()); + result.add(id); + } + log.debug("generated JavaScript bindings:\n" + js); + scriptEl.appendChild(xslTemplate.createTextNode(js.toString())); + compEl.setAttribute("functions", functionNames); + compEl.appendChild(scriptEl); + } + docEl.setAttribute("exclude-result-prefixes", StringUtils.join(excludePrefixes + .toArray(new String[excludePrefixes.size()]), " ")); + return result; + } + + /** + * Adds the specified parameters to the xsl template as variables within the alfresco namespace. + * + * @param xsltModel + * the variables to place within the xsl template + * @param xslTemplate + * the xsl template + */ + protected void addParameters(final XSLTemplateModel xsltModel, final Document xslTemplate) + { + final Element docEl = xslTemplate.getDocumentElement(); + final String XSL_NS = docEl.getNamespaceURI(); + final String XSL_NS_PREFIX = docEl.getPrefix(); + + for (Map.Entry e : xsltModel.entrySet()) + { + if (ROOT_NAMESPACE.equals(e.getKey())) + { + continue; + } + final Element el = xslTemplate.createElementNS(XSL_NS, XSL_NS_PREFIX + ":variable"); + el.setAttribute("name", e.getKey().toPrefixString()); + final Object o = e.getValue(); + if (o instanceof String || o instanceof Number || o instanceof Boolean) + { + el.appendChild(xslTemplate.createTextNode(o.toString())); + docEl.insertBefore(el, docEl.getFirstChild()); + } + } + } + + protected Source getXMLSource(final Map model) + { + if (!model.containsKey(ROOT_NAMESPACE)) + { + return null; + } + final Object o = model.get(ROOT_NAMESPACE); + if (!(o instanceof Document)) + { + throw new IllegalArgumentException("expected root namespace object to be a " + Document.class.getName() + + ". found a " + o.getClass().getName()); + } + return new DOMSource((Document) o); + } + +} diff --git a/source/java/org/alfresco/repo/template/XSLTProcessorMethodInvoker.java b/source/java/org/alfresco/repo/template/XSLTProcessorMethodInvoker.java new file mode 100644 index 0000000000..355d8b2bd4 --- /dev/null +++ b/source/java/org/alfresco/repo/template/XSLTProcessorMethodInvoker.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco 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, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Alfresco. If not, see . + */ + +package org.alfresco.repo.template; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.xml.dtm.ref.DTMNodeProxy; +import org.w3c.dom.DOMException; +import org.w3c.dom.Node; +import org.w3c.dom.traversal.NodeFilter; +import org.w3c.dom.traversal.NodeIterator; + +/** + * @author Brian + * + */ +public class XSLTProcessorMethodInvoker +{ + private final static Log log = LogFactory.getLog(XSLTProcessorMethodInvoker.class); + + private final static Map PROCESSOR_METHODS = new TreeMap(); + + public XSLTProcessorMethodInvoker() + { + } + + public static void addMethod(String name, TemplateProcessorMethod method) + { + PROCESSOR_METHODS.put(name, method); + } + + public static void removeMethods(Collection methodNames) + { + for (String methodName : methodNames) + { + PROCESSOR_METHODS.remove(methodName); + } + } + + private Object[] convertArguments(final Object[] arguments) + { + final List result = new LinkedList(); + for (int i = 0; i < arguments.length; i++) + { + log.debug("args[" + i + "] = " + arguments[i] + "(" + + (arguments[i] != null ? arguments[i].getClass().getName() : "null") + ")"); + if (arguments[i] == null || arguments[i] instanceof String || arguments[i] instanceof Number) + { + result.add(arguments[i]); + } + else if (arguments[i] instanceof DTMNodeProxy) + { + result.add(((DTMNodeProxy) arguments[i]).getStringValue()); + } + else if (arguments[i] instanceof Node) + { + log.debug("node type is " + ((Node) arguments[i]).getNodeType() + " content " + + ((Node) arguments[i]).getTextContent()); + result.add(((Node) arguments[i]).getNodeValue()); + } + else if (arguments[i] instanceof NodeIterator) + { + Node n = ((NodeIterator) arguments[i]).nextNode(); + while (n != null) + { + log.debug("iterated to node " + n + " type " + n.getNodeType() + " value " + n.getNodeValue() + + " tc " + n.getTextContent() + " nn " + n.getNodeName() + " sv " + + ((org.apache.xml.dtm.ref.DTMNodeProxy) n).getStringValue()); + if (n instanceof DTMNodeProxy) + { + result.add(((DTMNodeProxy) n).getStringValue()); + } + else + { + result.add(n); + } + n = ((NodeIterator) arguments[i]).nextNode(); + } + } + else + { + throw new IllegalArgumentException("unable to convert argument " + arguments[i]); + } + } + + return result.toArray(new Object[result.size()]); + } + + public Object invokeMethod(final String id, Object[] arguments) throws Exception + { + if (!PROCESSOR_METHODS.containsKey(id)) + { + throw new NullPointerException("unable to find method " + id); + } + + final TemplateProcessorMethod method = PROCESSOR_METHODS.get(id); + arguments = this.convertArguments(arguments); + log.debug("invoking " + id + " with " + arguments.length); + + Object result = method.exec(arguments); + log.debug(id + " returned a " + result); + if (result == null) + { + return null; + } + else if (result.getClass().isArray() && Node.class.isAssignableFrom(result.getClass().getComponentType())) + { + log.debug("converting " + result + " to a node iterator"); + final Node[] array = (Node[]) result; + return new NodeIterator() + { + private int index = 0; + private boolean detached = false; + + public void detach() + { + if (log.isDebugEnabled()) + log.debug("detaching NodeIterator"); + this.detached = true; + } + + public boolean getExpandEntityReferences() + { + return true; + } + + public int getWhatToShow() + { + return NodeFilter.SHOW_ALL; + } + + public Node getRoot() + { + return (array.length == 0 ? null : array[0].getOwnerDocument().getDocumentElement()); + } + + public NodeFilter getFilter() + { + return new NodeFilter() + { + public short acceptNode(final Node n) + { + return NodeFilter.FILTER_ACCEPT; + } + }; + } + + public Node nextNode() throws DOMException + { + if (log.isDebugEnabled()) + log.debug("NodeIterator.nextNode(" + index + ")"); + if (this.detached) + throw new DOMException(DOMException.INVALID_STATE_ERR, null); + return index == array.length ? null : array[index++]; + } + + public Node previousNode() throws DOMException + { + if (log.isDebugEnabled()) + log.debug("NodeIterator.previousNode(" + index + ")"); + if (this.detached) + throw new DOMException(DOMException.INVALID_STATE_ERR, null); + return index == -1 ? null : array[index--]; + } + }; + } + else if (result instanceof String || result instanceof Number || result instanceof Node) + { + log.debug("returning " + result + " as is"); + return result; + } + else + { + throw new IllegalArgumentException("unable to convert " + result.getClass().getName()); + } + } +} diff --git a/source/java/org/alfresco/repo/template/XSLTProcessorTest.java b/source/java/org/alfresco/repo/template/XSLTProcessorTest.java new file mode 100644 index 0000000000..21c9413a66 --- /dev/null +++ b/source/java/org/alfresco/repo/template/XSLTProcessorTest.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco 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, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Alfresco. If not, see . + */ + +package org.alfresco.repo.template; + +import java.io.StringWriter; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.rendition.executer.XSLTFunctions; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.TemplateProcessor; +import org.alfresco.service.cmr.repository.TemplateService; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.util.BaseAlfrescoSpringTest; +import org.alfresco.util.GUID; +import org.alfresco.util.XMLUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @author Brian + * + */ +public class XSLTProcessorTest extends BaseAlfrescoSpringTest +{ + private final static Log log = LogFactory.getLog(XSLTProcessorTest.class); + private XSLTFunctions xsltFunctions; + private SearchService searchService; + private NodeRef companyHome; + private FileFolderService fileFolderService; + private TemplateProcessor xsltProcessor; + private TemplateService templateService; + + /* + * (non-Javadoc) + * + * @see org.alfresco.util.BaseAlfrescoSpringTest#onSetUpInTransaction() + */ + @SuppressWarnings("deprecation") + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + this.searchService = (SearchService) this.applicationContext.getBean("SearchService"); + this.xsltFunctions = (XSLTFunctions) this.applicationContext.getBean("xsltFunctions"); + this.nodeService = (NodeService) this.applicationContext.getBean("NodeService"); + this.contentService = (ContentService) this.applicationContext.getBean("ContentService"); + this.fileFolderService = (FileFolderService) this.applicationContext.getBean("FileFolderService"); + this.xsltProcessor = (TemplateProcessor) this.applicationContext.getBean("xsltProcessor"); + this.templateService = (TemplateService) this.applicationContext.getBean("TemplateService"); + ResultSet rs = searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, SearchService.LANGUAGE_XPATH, + "/app:company_home"); + this.companyHome = rs.getNodeRef(0); + } + + public void testSimplestStringTemplate() throws Exception + { + try + { + FileInfo file = createXmlFile(companyHome); + XSLTemplateModel model = new XSLTemplateModel(); + model.put(XSLTProcessor.ROOT_NAMESPACE, XMLUtil.parse(file.getNodeRef(), contentService)); + + StringWriter writer = new StringWriter(); + xsltProcessor.processString(verySimpleXSLT, model, writer); + String output = writer.toString(); + + log.debug("XSLT Processor output: " + output); + assertEquals("Avocado DipBagels, New York StyleBeef Frankfurter, Quarter PoundChicken Pot PieCole SlawEggsHazelnut SpreadPotato ChipsSoy Patties, GrilledTruffles, Dark Chocolate", output); + } + catch (Exception ex) + { + log.error("Error!", ex); + fail(); + } + } + + public void testSimplestNodeTemplate() throws Exception + { + try + { + FileInfo xmlFile = createXmlFile(companyHome); + FileInfo xslFile = createXmlFile(companyHome, verySimpleXSLT); + XSLTemplateModel model = new XSLTemplateModel(); + model.put(XSLTProcessor.ROOT_NAMESPACE, XMLUtil.parse(xmlFile.getNodeRef(), contentService)); + + StringWriter writer = new StringWriter(); + xsltProcessor.process(xslFile.getNodeRef().toString(), model, writer); + + String output = writer.toString(); + + log.debug("XSLT Processor output: " + output); + assertEquals("Avocado DipBagels, New York StyleBeef Frankfurter, Quarter PoundChicken Pot PieCole SlawEggsHazelnut SpreadPotato ChipsSoy Patties, GrilledTruffles, Dark Chocolate", output); + } + catch (Exception ex) + { + log.error("Error!", ex); + fail(); + } + } + + public void testSimplestClasspathTemplate() throws Exception + { + try + { + FileInfo xmlFile = createXmlFile(companyHome); + XSLTemplateModel model = new XSLTemplateModel(); + model.put(XSLTProcessor.ROOT_NAMESPACE, XMLUtil.parse(xmlFile.getNodeRef(), contentService)); + + StringWriter writer = new StringWriter(); + xsltProcessor.process("org/alfresco/repo/template/test_template1.xsl", model, writer); + + String output = writer.toString(); + + log.debug("XSLT Processor output: " + output); + assertEquals("Avocado DipBagels, New York StyleBeef Frankfurter, Quarter PoundChicken Pot PieCole SlawEggsHazelnut SpreadPotato ChipsSoy Patties, GrilledTruffles, Dark Chocolate", output); + } + catch (Exception ex) + { + log.error("Error!", ex); + fail(); + } + } + + public void testTemplateServiceBinding() throws Exception + { + try + { + FileInfo xmlFile = createXmlFile(companyHome); + XSLTemplateModel model = new XSLTemplateModel(); + model.put(XSLTProcessor.ROOT_NAMESPACE, XMLUtil.parse(xmlFile.getNodeRef(), contentService)); + + StringWriter writer = new StringWriter(); + templateService.processTemplate("xslt", "org/alfresco/repo/template/test_template1.xsl", model, writer); + + String output = writer.toString(); + + log.debug("XSLT Processor output: " + output); + assertEquals("Avocado DipBagels, New York StyleBeef Frankfurter, Quarter PoundChicken Pot PieCole SlawEggsHazelnut SpreadPotato ChipsSoy Patties, GrilledTruffles, Dark Chocolate", output); + } + catch (Exception ex) + { + log.error("Error!", ex); + fail(); + } + } + + + + private FileInfo createXmlFile(NodeRef folder) + { + return createXmlFile(folder, sampleXML); + } + + private FileInfo createXmlFile(NodeRef folder, String content) + { + String name = GUID.generate(); + FileInfo testXmlFile = fileFolderService.create(folder, name + ".xml", ContentModel.TYPE_CONTENT); + ContentWriter writer = contentService.getWriter(testXmlFile.getNodeRef(), ContentModel.PROP_CONTENT, true); + writer.setMimetype("text/xml"); + writer.setEncoding("UTF-8"); + writer.putContent(content); + return testXmlFile; + } + + private String sampleXML = "" + "" + + + "" + "65" + "20" + + "300" + "2400" + + "300" + "25" + "50" + + "" + + + "" + "Avocado Dip" + "Sunnydale" + "29" + + "" + "11" + + "3" + "5" + "210" + + "2" + "0" + "1" + "" + "0" + "0" + + "" + "" + "0" + "0" + "" + "" + + + "" + "Bagels, New York Style" + "Thompson" + + "104" + "" + + "4" + "1" + "0" + + "510" + "54" + "3" + "11" + "" + + "0" + "0" + "" + "" + "8" + "20" + "" + + "" + + + "" + "Beef Frankfurter, Quarter Pound" + "Armitage" + + "115" + "" + + "32" + "15" + "65" + + "1100" + "8" + "0" + "13" + "" + + "0" + "2" + "" + "" + "1" + "6" + "" + + "" + + + "" + "Chicken Pot Pie" + "Lakeson" + "198" + + "" + "22" + + "9" + "25" + "810" + + "42" + "2" + "10" + "" + "20" + + "2" + "" + "" + "2" + "10" + "" + "" + + + "" + "Cole Slaw" + "Fresh Quick" + "1.5" + + "" + "0" + "0" + + "0" + "15" + "5" + "2" + + "1" + "" + "30" + "45" + "" + "" + + "4" + "2" + "" + "" + + + "" + "Eggs" + "Goodpath" + "50" + + "" + "4.5" + + "1.5" + "215" + "65" + + "1" + "0" + "6" + "" + "6" + "0" + + "" + "" + "2" + "4" + "" + "" + + + "" + "Hazelnut Spread" + "Ferreira" + "2" + + "" + "10" + "2" + + "0" + "20" + "23" + "2" + + "3" + "" + "0" + "0" + "" + "" + + "6" + "4" + "" + "" + + + "" + "Potato Chips" + "Lees" + "28" + + "" + "10" + "3" + + "0" + "180" + "15" + "1" + + "2" + "" + "0" + "10" + "" + "" + + "0" + "0" + "" + "" + + + "" + "Soy Patties, Grilled" + "Gardenproducts" + + "96" + "" + "5" + + "0" + "0" + "420" + + "10" + "4" + "9" + "" + "0" + "0" + + "" + "" + "0" + "0" + "" + "" + + + "" + "Truffles, Dark Chocolate" + "Lyndon's" + + "39" + "" + + "19" + "14" + "25" + + "10" + "16" + "1" + "1" + "" + + "0" + "0" + "" + "" + "0" + "0" + "" + + "" + + + ""; + + + private String verySimpleXSLT = "" + " " + "" + + + "" + + + "" + "" + + "" + "" + "" + ""; +} diff --git a/source/java/org/alfresco/repo/template/XSLTemplateModel.java b/source/java/org/alfresco/repo/template/XSLTemplateModel.java new file mode 100644 index 0000000000..e87473a035 --- /dev/null +++ b/source/java/org/alfresco/repo/template/XSLTemplateModel.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco 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, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Alfresco. If not, see . + */ +package org.alfresco.repo.template; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.namespace.QName; + +public class XSLTemplateModel implements Map +{ + private Map wrappedMap = new HashMap(); + + /** + * + * @see java.util.Map#clear() + */ + public void clear() + { + wrappedMap.clear(); + } + + /** + * @param key + * @return + * @see java.util.Map#containsKey(java.lang.Object) + */ + public boolean containsKey(Object key) + { + return wrappedMap.containsKey(key); + } + + /** + * @param value + * @return + * @see java.util.Map#containsValue(java.lang.Object) + */ + public boolean containsValue(Object value) + { + return wrappedMap.containsValue(value); + } + + /** + * @return + * @see java.util.Map#entrySet() + */ + public Set> entrySet() + { + return wrappedMap.entrySet(); + } + + /** + * @param o + * @return + * @see java.util.Map#equals(java.lang.Object) + */ + public boolean equals(Object o) + { + return wrappedMap.equals(o); + } + + /** + * @param key + * @return + * @see java.util.Map#get(java.lang.Object) + */ + public Object get(Object key) + { + return wrappedMap.get(key); + } + + /** + * @return + * @see java.util.Map#hashCode() + */ + public int hashCode() + { + return wrappedMap.hashCode(); + } + + /** + * @return + * @see java.util.Map#isEmpty() + */ + public boolean isEmpty() + { + return wrappedMap.isEmpty(); + } + + /** + * @return + * @see java.util.Map#keySet() + */ + public Set keySet() + { + return wrappedMap.keySet(); + } + + /** + * @param key + * @param value + * @return + * @see java.util.Map#put(java.lang.Object, java.lang.Object) + */ + public Object put(QName key, Object value) + { + return wrappedMap.put(key, value); + } + + /** + * @param m + * @see java.util.Map#putAll(java.util.Map) + */ + public void putAll(Map m) + { + wrappedMap.putAll(m); + } + + /** + * @param key + * @return + * @see java.util.Map#remove(java.lang.Object) + */ + public Object remove(Object key) + { + return wrappedMap.remove(key); + } + + /** + * @return + * @see java.util.Map#size() + */ + public int size() + { + return wrappedMap.size(); + } + + /** + * @return + * @see java.util.Map#values() + */ + public Collection values() + { + return wrappedMap.values(); + } + + +} diff --git a/source/java/org/alfresco/repo/template/test_template1.xsl b/source/java/org/alfresco/repo/template/test_template1.xsl new file mode 100644 index 0000000000..3308b738f2 --- /dev/null +++ b/source/java/org/alfresco/repo/template/test_template1.xsl @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/source/java/org/alfresco/util/XMLUtil.java b/source/java/org/alfresco/util/XMLUtil.java index d7a60826c8..683e356805 100644 --- a/source/java/org/alfresco/util/XMLUtil.java +++ b/source/java/org/alfresco/util/XMLUtil.java @@ -1,28 +1,26 @@ /* - * Copyright (C) 2005-2008 Alfresco Software Limited. + * Copyright (C) 2005-2010 Alfresco Software Limited. * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - - * This program is distributed in the hope that it will be useful, + * This file is part of Alfresco + * + * Alfresco 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, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 General Public License for more details. + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * As a special exception to the terms and conditions of version 2.0 of - * the GPL, you may redistribute this Program in connection with Free/Libre - * and Open Source Software ("FLOSS") applications as described in Alfresco's - * FLOSS exception. You should have recieved a copy of the text describing - * the FLOSS exception, and it is also available here: - * http://www.alfresco.com/legal/licensing" */ package org.alfresco.util; import java.io.*; + import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -40,6 +38,7 @@ import org.alfresco.service.cmr.repository.NodeRef; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.w3c.dom.*; +import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** @@ -168,6 +167,22 @@ public class XMLUtil } } + /** utility function for parsing xml */ + public static Document parse(final Reader source) + throws SAXException, + IOException + { + try + { + final DocumentBuilder db = XMLUtil.getDocumentBuilder(); + return db.parse(new InputSource(source)); + } + finally + { + source.close(); + } + } + /** provides a document builder that is namespace aware but not validating by default */ public static DocumentBuilder getDocumentBuilder() {