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}