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