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