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