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}