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 volatile Map<String, List<String>> type2Template; 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 String query = "select * from Document where ecm:mixinType = 'Template' AND ecm:isTrashed = 0"; 247 if (Boolean.parseBoolean(Framework.getProperty(FILTER_VERSIONS_PROPERTY))) { 248 query += " AND ecm:isVersion = 0"; 249 } 250 if (targetType != null) { 251 query += " AND tmpl:applicableTypes IN ( 'all', '" + targetType + "')"; 252 } 253 return query; 254 } 255 256 protected String buildTemplateSearchByNameQuery(String name) { 257 String query = "select * from Document where ecm:mixinType = 'Template' " 258 + "AND tmpl:templateName = " + NXQL.escapeString(name); 259 if (Boolean.parseBoolean(Framework.getProperty(FILTER_VERSIONS_PROPERTY))) { 260 query += " AND ecm:isVersion = 0"; 261 } 262 return query; 263 } 264 265 @Override 266 public List<DocumentModel> getAvailableTemplateDocs(CoreSession session, String targetType) { 267 return session.query(buildTemplateSearchQuery(targetType)); 268 } 269 270 @Override 271 public DocumentModel getTemplateDoc(CoreSession session, String name) { 272 String query = buildTemplateSearchByNameQuery(name); 273 List<DocumentModel> docs = session.query(query); 274 return docs.size() == 0 ? null : docs.get(0); 275 } 276 277 protected <T> List<T> wrap(List<DocumentModel> docs, Class<T> adapter) { 278 List<T> result = new ArrayList<>(); 279 for (DocumentModel doc : docs) { 280 T adapted = doc.getAdapter(adapter); 281 if (adapted != null) { 282 result.add(adapted); 283 } 284 } 285 return result; 286 } 287 288 @Override 289 public List<TemplateSourceDocument> getAvailableOfficeTemplates(CoreSession session, String targetType) { 290 String query = buildTemplateSearchQuery(targetType) + " AND tmpl:useAsMainContent=1"; 291 List<DocumentModel> docs = session.query(query); 292 return wrap(docs, TemplateSourceDocument.class); 293 } 294 295 @Override 296 public List<TemplateSourceDocument> getAvailableTemplates(CoreSession session, String targetType) { 297 List<DocumentModel> filtredResult = getAvailableTemplateDocs(session, targetType); 298 return wrap(filtredResult, TemplateSourceDocument.class); 299 } 300 301 @Override 302 public List<TemplateBasedDocument> getLinkedTemplateBasedDocuments(DocumentModel source) { 303 StringBuilder sb = new StringBuilder() 304 .append("select * from Document where ecm:isVersion = 0 AND ecm:isProxy = 0 AND ") 305 .append(TemplateBindings.BINDING_PROP_NAME) 306 .append("/*/") 307 .append(TemplateBinding.TEMPLATE_ID_KEY) 308 .append(" = '") 309 .append(source.getId()) 310 .append("'"); 311 DocumentModelList docs = source.getCoreSession().query(sb.toString()); 312 313 List<TemplateBasedDocument> result = new ArrayList<>(); 314 for (DocumentModel doc : docs) { 315 TemplateBasedDocument templateBasedDocument = doc.getAdapter(TemplateBasedDocument.class); 316 if (templateBasedDocument != null) { 317 result.add(templateBasedDocument); 318 } 319 } 320 return result; 321 } 322 323 @Override 324 public Collection<TemplateProcessorDescriptor> getRegisteredTemplateProcessors() { 325 return processorRegistry.getRegistredProcessors(); 326 } 327 328 @Override 329 public Map<String, List<String>> getTypeMapping() { 330 if (type2Template == null) { 331 synchronized (this) { 332 if (type2Template == null) { 333 Map<String, List<String>> map = new ConcurrentHashMap<>(); 334 TemplateMappingFetcher fetcher = new TemplateMappingFetcher(); 335 fetcher.runUnrestricted(); 336 map.putAll(fetcher.getMapping()); 337 type2Template = map; 338 } 339 } 340 } 341 return type2Template; 342 } 343 344 @Override 345 public synchronized void registerTypeMapping(DocumentModel doc) { 346 TemplateSourceDocument tmpl = doc.getAdapter(TemplateSourceDocument.class); 347 if (tmpl != null) { 348 Map<String, List<String>> mapping = getTypeMapping(); 349 // check existing mapping for this docId 350 List<String> boundTypes = new ArrayList<>(); 351 for (String type : mapping.keySet()) { 352 if (mapping.get(type) != null) { 353 if (mapping.get(type).contains(doc.getId())) { 354 boundTypes.add(type); 355 } 356 } 357 } 358 // unbind previous mapping for this docId 359 for (String type : boundTypes) { 360 List<String> templates = mapping.get(type); 361 templates.remove(doc.getId()); 362 if (templates.size() == 0) { 363 mapping.remove(type); 364 } 365 } 366 // rebind types (with override) 367 for (String type : tmpl.getForcedTypes()) { 368 List<String> templates = mapping.get(type); 369 if (templates == null) { 370 templates = new ArrayList<>(); 371 mapping.put(type, templates); 372 } 373 if (!templates.contains(doc.getId())) { 374 templates.add(doc.getId()); 375 } 376 } 377 } 378 } 379 380 @Override 381 public DocumentModel makeTemplateBasedDocument(DocumentModel targetDoc, DocumentModel sourceTemplateDoc, 382 boolean save) { 383 targetDoc.addFacet(TemplateBasedDocumentAdapterImpl.TEMPLATEBASED_FACET); 384 TemplateBasedDocument tmplBased = targetDoc.getAdapter(TemplateBasedDocument.class); 385 // bind the template 386 return tmplBased.setTemplate(sourceTemplateDoc, save); 387 } 388 389 @Override 390 public DocumentModel detachTemplateBasedDocument(DocumentModel targetDoc, String templateName, boolean save) { 391 DocumentModel docAfterDetach = null; 392 TemplateBasedDocument tbd = targetDoc.getAdapter(TemplateBasedDocument.class); 393 if (tbd != null) { 394 if (!tbd.getTemplateNames().contains(templateName)) { 395 return targetDoc; 396 } 397 if (tbd.getTemplateNames().size() == 1) { 398 // remove the whole facet since there is no more binding 399 targetDoc.removeFacet(TemplateBasedDocumentAdapterImpl.TEMPLATEBASED_FACET); 400 if (log.isDebugEnabled()) { 401 log.debug("detach after removeFacet, ck=" + targetDoc.getCacheKey()); 402 } 403 if (save) { 404 docAfterDetach = targetDoc.getCoreSession().saveDocument(targetDoc); 405 } 406 } else { 407 // only remove the binding 408 docAfterDetach = tbd.removeTemplateBinding(templateName, true); 409 } 410 } 411 if (docAfterDetach != null) { 412 return docAfterDetach; 413 } 414 return targetDoc; 415 } 416 417 @Override 418 public Collection<OutputFormatDescriptor> getOutputFormats() { 419 return outputFormatRegistry.getRegistredOutputFormat(); 420 } 421 422 @Override 423 public OutputFormatDescriptor getOutputFormatDescriptor(String outputFormatId) { 424 return outputFormatRegistry.getOutputFormatById(outputFormatId); 425 } 426}