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}