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:[email protected]">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 StringBuffer sb = new StringBuffer( 247 "select * from Document where ecm:mixinType = 'Template' AND ecm:isTrashed = 0"); 248 if (Boolean.parseBoolean(Framework.getProperty(FILTER_VERSIONS_PROPERTY))) { 249 sb.append(" AND ecm:isVersion = 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:isVersion = 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:isVersion = 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 Map<String, List<String>> map = new ConcurrentHashMap<>(); 337 TemplateMappingFetcher fetcher = new TemplateMappingFetcher(); 338 fetcher.runUnrestricted(); 339 map.putAll(fetcher.getMapping()); 340 type2Template = map; 341 } 342 } 343 } 344 return type2Template; 345 } 346 347 @Override 348 public synchronized void registerTypeMapping(DocumentModel doc) { 349 TemplateSourceDocument tmpl = doc.getAdapter(TemplateSourceDocument.class); 350 if (tmpl != null) { 351 Map<String, List<String>> mapping = getTypeMapping(); 352 // check existing mapping for this docId 353 List<String> boundTypes = new ArrayList<>(); 354 for (String type : mapping.keySet()) { 355 if (mapping.get(type) != null) { 356 if (mapping.get(type).contains(doc.getId())) { 357 boundTypes.add(type); 358 } 359 } 360 } 361 // unbind previous mapping for this docId 362 for (String type : boundTypes) { 363 List<String> templates = mapping.get(type); 364 templates.remove(doc.getId()); 365 if (templates.size() == 0) { 366 mapping.remove(type); 367 } 368 } 369 // rebind types (with override) 370 for (String type : tmpl.getForcedTypes()) { 371 List<String> templates = mapping.get(type); 372 if (templates == null) { 373 templates = new ArrayList<>(); 374 mapping.put(type, templates); 375 } 376 if (!templates.contains(doc.getId())) { 377 templates.add(doc.getId()); 378 } 379 } 380 } 381 } 382 383 @Override 384 public DocumentModel makeTemplateBasedDocument(DocumentModel targetDoc, DocumentModel sourceTemplateDoc, 385 boolean save) { 386 targetDoc.addFacet(TemplateBasedDocumentAdapterImpl.TEMPLATEBASED_FACET); 387 TemplateBasedDocument tmplBased = targetDoc.getAdapter(TemplateBasedDocument.class); 388 // bind the template 389 return tmplBased.setTemplate(sourceTemplateDoc, save); 390 } 391 392 @Override 393 public DocumentModel detachTemplateBasedDocument(DocumentModel targetDoc, String templateName, boolean save) 394 { 395 DocumentModel docAfterDetach = null; 396 TemplateBasedDocument tbd = targetDoc.getAdapter(TemplateBasedDocument.class); 397 if (tbd != null) { 398 if (!tbd.getTemplateNames().contains(templateName)) { 399 return targetDoc; 400 } 401 if (tbd.getTemplateNames().size() == 1) { 402 // remove the whole facet since there is no more binding 403 targetDoc.removeFacet(TemplateBasedDocumentAdapterImpl.TEMPLATEBASED_FACET); 404 if (log.isDebugEnabled()) { 405 log.debug("detach after removeFacet, ck=" + targetDoc.getCacheKey()); 406 } 407 if (save) { 408 docAfterDetach = targetDoc.getCoreSession().saveDocument(targetDoc); 409 } 410 } else { 411 // only remove the binding 412 docAfterDetach = tbd.removeTemplateBinding(templateName, true); 413 } 414 } 415 if (docAfterDetach != null) { 416 return docAfterDetach; 417 } 418 return targetDoc; 419 } 420 421 @Override 422 public Collection<OutputFormatDescriptor> getOutputFormats() { 423 return outputFormatRegistry.getRegistredOutputFormat(); 424 } 425 426 @Override 427 public OutputFormatDescriptor getOutputFormatDescriptor(String outputFormatId) { 428 return outputFormatRegistry.getOutputFormatById(outputFormatId); 429 } 430}