001/* 002 * (C) Copyright 2012-2015 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Thierry Delprat 016 * 017 */ 018package org.nuxeo.template.service; 019 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.List; 023import java.util.Map; 024import java.util.concurrent.ConcurrentHashMap; 025 026import org.apache.commons.logging.Log; 027import org.apache.commons.logging.LogFactory; 028import org.nuxeo.common.utils.FileUtils; 029import org.nuxeo.ecm.core.api.Blob; 030import org.nuxeo.ecm.core.api.CoreSession; 031import org.nuxeo.ecm.core.api.DocumentModel; 032import org.nuxeo.ecm.core.api.DocumentModelList; 033import org.nuxeo.runtime.api.Framework; 034import org.nuxeo.runtime.model.ComponentContext; 035import org.nuxeo.runtime.model.ComponentInstance; 036import org.nuxeo.runtime.model.DefaultComponent; 037import org.nuxeo.template.adapters.doc.TemplateBasedDocumentAdapterImpl; 038import org.nuxeo.template.adapters.doc.TemplateBinding; 039import org.nuxeo.template.adapters.doc.TemplateBindings; 040import org.nuxeo.template.api.TemplateProcessor; 041import org.nuxeo.template.api.TemplateProcessorService; 042import org.nuxeo.template.api.adapters.TemplateBasedDocument; 043import org.nuxeo.template.api.adapters.TemplateSourceDocument; 044import org.nuxeo.template.api.context.ContextExtensionFactory; 045import org.nuxeo.template.api.context.DocumentWrapper; 046import org.nuxeo.template.api.descriptor.ContextExtensionFactoryDescriptor; 047import org.nuxeo.template.api.descriptor.OutputFormatDescriptor; 048import org.nuxeo.template.api.descriptor.TemplateProcessorDescriptor; 049import org.nuxeo.template.context.AbstractContextBuilder; 050import org.nuxeo.template.fm.FreeMarkerVariableExtractor; 051import org.nuxeo.template.processors.IdentityProcessor; 052 053/** 054 * Runtime Component used to handle Extension Points and expose the {@link TemplateProcessorService} interface 055 * 056 * @author <a href="mailto:tdelprat@nuxeo.com">Tiry</a> 057 */ 058public class TemplateProcessorComponent extends DefaultComponent implements TemplateProcessorService { 059 060 protected static final Log log = LogFactory.getLog(TemplateProcessorComponent.class); 061 062 public static final String PROCESSOR_XP = "processor"; 063 064 public static final String CONTEXT_EXTENSION_XP = "contextExtension"; 065 066 public static final String OUTPUT_FORMAT_EXTENSION_XP = "outputFormat"; 067 068 private static final String FILTER_VERSIONS_PROPERTY = "nuxeo.templating.filterVersions"; 069 070 protected ContextFactoryRegistry contextExtensionRegistry; 071 072 protected TemplateProcessorRegistry processorRegistry; 073 074 protected OutputFormatRegistry outputFormatRegistry; 075 076 protected ConcurrentHashMap<String, List<String>> type2Template = null; 077 078 @Override 079 public void activate(ComponentContext context) { 080 processorRegistry = new TemplateProcessorRegistry(); 081 contextExtensionRegistry = new ContextFactoryRegistry(); 082 outputFormatRegistry = new OutputFormatRegistry(); 083 } 084 085 @Override 086 public void deactivate(ComponentContext context) { 087 processorRegistry = null; 088 contextExtensionRegistry = null; 089 outputFormatRegistry = null; 090 } 091 092 @Override 093 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 094 if (PROCESSOR_XP.equals(extensionPoint)) { 095 processorRegistry.addContribution((TemplateProcessorDescriptor) contribution); 096 } else if (CONTEXT_EXTENSION_XP.equals(extensionPoint)) { 097 contextExtensionRegistry.addContribution((ContextExtensionFactoryDescriptor) contribution); 098 // force recompute of reserved keywords 099 FreeMarkerVariableExtractor.resetReservedContextKeywords(); 100 } else if (OUTPUT_FORMAT_EXTENSION_XP.equals(extensionPoint)) { 101 outputFormatRegistry.addContribution((OutputFormatDescriptor) contribution); 102 } 103 } 104 105 @Override 106 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 107 if (PROCESSOR_XP.equals(extensionPoint)) { 108 processorRegistry.removeContribution((TemplateProcessorDescriptor) contribution); 109 } else if (CONTEXT_EXTENSION_XP.equals(extensionPoint)) { 110 contextExtensionRegistry.removeContribution((ContextExtensionFactoryDescriptor) contribution); 111 } else if (OUTPUT_FORMAT_EXTENSION_XP.equals(extensionPoint)) { 112 outputFormatRegistry.removeContribution((OutputFormatDescriptor) contribution); 113 } 114 } 115 116 @Override 117 public TemplateProcessor findProcessor(Blob templateBlob) { 118 TemplateProcessorDescriptor desc = findProcessorDescriptor(templateBlob); 119 if (desc != null) { 120 return desc.getProcessor(); 121 } else { 122 return null; 123 } 124 } 125 126 @Override 127 public String findProcessorName(Blob templateBlob) { 128 TemplateProcessorDescriptor desc = findProcessorDescriptor(templateBlob); 129 if (desc != null) { 130 return desc.getName(); 131 } else { 132 return null; 133 } 134 } 135 136 public TemplateProcessorDescriptor findProcessorDescriptor(Blob templateBlob) { 137 TemplateProcessorDescriptor processor = null; 138 String mt = templateBlob.getMimeType(); 139 if (mt != null) { 140 processor = findProcessorByMimeType(mt); 141 } 142 if (processor == null) { 143 String fileName = templateBlob.getFilename(); 144 if (fileName != null) { 145 String ext = FileUtils.getFileExtension(fileName); 146 processor = findProcessorByExtension(ext); 147 } 148 } 149 return processor; 150 } 151 152 @Override 153 public void addContextExtensions(DocumentModel currentDocument, DocumentWrapper wrapper, Map<String, Object> ctx) { 154 Map<String, ContextExtensionFactoryDescriptor> factories = contextExtensionRegistry.getExtensionFactories(); 155 for (String name : factories.keySet()) { 156 ContextExtensionFactory factory = factories.get(name).getExtensionFactory(); 157 if (factory != null) { 158 Object ob = factory.getExtension(currentDocument, wrapper, ctx); 159 if (ob != null) { 160 ctx.put(name, ob); 161 // also manage aliases 162 for (String alias : factories.get(name).getAliases()) { 163 ctx.put(alias, ob); 164 } 165 } 166 } 167 } 168 } 169 170 @Override 171 public List<String> getReservedContextKeywords() { 172 List<String> keywords = new ArrayList<>(); 173 Map<String, ContextExtensionFactoryDescriptor> factories = contextExtensionRegistry.getExtensionFactories(); 174 for (String name : factories.keySet()) { 175 keywords.add(name); 176 keywords.addAll(factories.get(name).getAliases()); 177 } 178 for (String keyword : AbstractContextBuilder.RESERVED_VAR_NAMES) { 179 keywords.add(keyword); 180 } 181 return keywords; 182 } 183 184 @Override 185 public Map<String, ContextExtensionFactoryDescriptor> getRegistredContextExtensions() { 186 return contextExtensionRegistry.getExtensionFactories(); 187 } 188 189 protected TemplateProcessorDescriptor findProcessorByMimeType(String mt) { 190 List<TemplateProcessorDescriptor> candidates = new ArrayList<>(); 191 for (TemplateProcessorDescriptor desc : processorRegistry.getRegistredProcessors()) { 192 if (desc.getSupportedMimeTypes().contains(mt)) { 193 if (desc.isDefaultProcessor()) { 194 return desc; 195 } else { 196 candidates.add(desc); 197 } 198 } 199 } 200 if (candidates.size() > 0) { 201 return candidates.get(0); 202 } 203 return null; 204 } 205 206 protected TemplateProcessorDescriptor findProcessorByExtension(String extension) { 207 List<TemplateProcessorDescriptor> candidates = new ArrayList<>(); 208 for (TemplateProcessorDescriptor desc : processorRegistry.getRegistredProcessors()) { 209 if (desc.getSupportedExtensions().contains(extension)) { 210 if (desc.isDefaultProcessor()) { 211 return desc; 212 } else { 213 candidates.add(desc); 214 } 215 } 216 } 217 if (candidates.size() > 0) { 218 return candidates.get(0); 219 } 220 return null; 221 } 222 223 public TemplateProcessorDescriptor getDescriptor(String name) { 224 return processorRegistry.getProcessorByName(name); 225 } 226 227 @Override 228 public TemplateProcessor getProcessor(String name) { 229 if (name == null) { 230 log.info("no defined processor name, using Identity as default"); 231 name = IdentityProcessor.NAME; 232 } 233 TemplateProcessorDescriptor desc = processorRegistry.getProcessorByName(name); 234 if (desc != null) { 235 return desc.getProcessor(); 236 } else { 237 log.warn("Can not get a TemplateProcessor with name " + name); 238 return null; 239 } 240 } 241 242 protected String buildTemplateSearchQuery(String targetType) { 243 StringBuffer sb = new StringBuffer( 244 "select * from Document where ecm:mixinType = 'Template' AND ecm:currentLifeCycleState != 'deleted'"); 245 if (Boolean.parseBoolean(Framework.getProperty(FILTER_VERSIONS_PROPERTY))) { 246 sb.append(" AND ecm:isCheckedInVersion = 0"); 247 } 248 if (targetType != null) { 249 sb.append(" AND tmpl:applicableTypes IN ( 'all', '" + targetType + "')"); 250 } 251 return sb.toString(); 252 } 253 254 @Override 255 public List<DocumentModel> getAvailableTemplateDocs(CoreSession session, String targetType) { 256 String query = buildTemplateSearchQuery(targetType); 257 return session.query(query); 258 } 259 260 protected <T> List<T> wrap(List<DocumentModel> docs, Class<T> adapter) { 261 List<T> result = new ArrayList<>(); 262 for (DocumentModel doc : docs) { 263 T adapted = doc.getAdapter(adapter); 264 if (adapted != null) { 265 result.add(adapted); 266 } 267 } 268 return result; 269 } 270 271 @Override 272 public List<TemplateSourceDocument> getAvailableOfficeTemplates(CoreSession session, String targetType) 273 { 274 String query = buildTemplateSearchQuery(targetType); 275 query = query + " AND tmpl:useAsMainContent=1"; 276 List<DocumentModel> docs = session.query(query); 277 return wrap(docs, TemplateSourceDocument.class); 278 } 279 280 @Override 281 public List<TemplateSourceDocument> getAvailableTemplates(CoreSession session, String targetType) 282 { 283 List<DocumentModel> filtredResult = getAvailableTemplateDocs(session, targetType); 284 return wrap(filtredResult, TemplateSourceDocument.class); 285 } 286 287 @Override 288 public List<TemplateBasedDocument> getLinkedTemplateBasedDocuments(DocumentModel source) { 289 StringBuffer sb = new StringBuffer( 290 "select * from Document where ecm:isCheckedInVersion = 0 AND ecm:isProxy = 0 AND "); 291 sb.append(TemplateBindings.BINDING_PROP_NAME + "/*/" + TemplateBinding.TEMPLATE_ID_KEY); 292 sb.append(" = '"); 293 sb.append(source.getId()); 294 sb.append("'"); 295 DocumentModelList docs = source.getCoreSession().query(sb.toString()); 296 297 List<TemplateBasedDocument> result = new ArrayList<>(); 298 for (DocumentModel doc : docs) { 299 TemplateBasedDocument templateBasedDocument = doc.getAdapter(TemplateBasedDocument.class); 300 if (templateBasedDocument != null) { 301 result.add(templateBasedDocument); 302 } 303 } 304 return result; 305 } 306 307 @Override 308 public Collection<TemplateProcessorDescriptor> getRegisteredTemplateProcessors() { 309 return processorRegistry.getRegistredProcessors(); 310 } 311 312 @Override 313 public Map<String, List<String>> getTypeMapping() { 314 if (type2Template == null) { 315 synchronized (this) { 316 if (type2Template == null) { 317 type2Template = new ConcurrentHashMap<>(); 318 TemplateMappingFetcher fetcher = new TemplateMappingFetcher(); 319 fetcher.runUnrestricted(); 320 type2Template.putAll(fetcher.getMapping()); 321 } 322 } 323 } 324 return type2Template; 325 } 326 327 @Override 328 public synchronized void registerTypeMapping(DocumentModel doc) { 329 TemplateSourceDocument tmpl = doc.getAdapter(TemplateSourceDocument.class); 330 if (tmpl != null) { 331 Map<String, List<String>> mapping = getTypeMapping(); 332 // check existing mapping for this docId 333 List<String> boundTypes = new ArrayList<>(); 334 for (String type : mapping.keySet()) { 335 if (mapping.get(type) != null) { 336 if (mapping.get(type).contains(doc.getId())) { 337 boundTypes.add(type); 338 } 339 } 340 } 341 // unbind previous mapping for this docId 342 for (String type : boundTypes) { 343 List<String> templates = mapping.get(type); 344 templates.remove(doc.getId()); 345 if (templates.size() == 0) { 346 mapping.remove(type); 347 } 348 } 349 // rebind types (with override) 350 for (String type : tmpl.getForcedTypes()) { 351 List<String> templates = mapping.get(type); 352 if (templates == null) { 353 templates = new ArrayList<>(); 354 mapping.put(type, templates); 355 } 356 if (!templates.contains(doc.getId())) { 357 templates.add(doc.getId()); 358 } 359 } 360 } 361 } 362 363 @Override 364 public DocumentModel makeTemplateBasedDocument(DocumentModel targetDoc, DocumentModel sourceTemplateDoc, 365 boolean save) { 366 targetDoc.addFacet(TemplateBasedDocumentAdapterImpl.TEMPLATEBASED_FACET); 367 TemplateBasedDocument tmplBased = targetDoc.getAdapter(TemplateBasedDocument.class); 368 // bind the template 369 return tmplBased.setTemplate(sourceTemplateDoc, save); 370 } 371 372 @Override 373 public DocumentModel detachTemplateBasedDocument(DocumentModel targetDoc, String templateName, boolean save) 374 { 375 DocumentModel docAfterDetach = null; 376 TemplateBasedDocument tbd = targetDoc.getAdapter(TemplateBasedDocument.class); 377 if (tbd != null) { 378 if (!tbd.getTemplateNames().contains(templateName)) { 379 return targetDoc; 380 } 381 if (tbd.getTemplateNames().size() == 1) { 382 // remove the whole facet since there is no more binding 383 targetDoc.removeFacet(TemplateBasedDocumentAdapterImpl.TEMPLATEBASED_FACET); 384 if (log.isDebugEnabled()) { 385 log.debug("detach after removeFacet, ck=" + targetDoc.getCacheKey()); 386 } 387 if (save) { 388 docAfterDetach = targetDoc.getCoreSession().saveDocument(targetDoc); 389 } 390 } else { 391 // only remove the binding 392 docAfterDetach = tbd.removeTemplateBinding(templateName, true); 393 } 394 } 395 if (docAfterDetach != null) { 396 return docAfterDetach; 397 } 398 return targetDoc; 399 } 400 401 @Override 402 public Collection<OutputFormatDescriptor> getOutputFormats() { 403 return outputFormatRegistry.getRegistredOutputFormat(); 404 } 405 406 @Override 407 public OutputFormatDescriptor getOutputFormatDescriptor(String outputFormatId) { 408 return outputFormatRegistry.getOutputFormatById(outputFormatId); 409 } 410}