001/*
002 *  Copyright 2013 Anyware Services
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 */
016package org.ametys.cms.contenttype;
017
018import java.io.File;
019import java.io.FileInputStream;
020import java.io.FilenameFilter;
021import java.io.InputStream;
022import java.util.Arrays;
023import java.util.HashSet;
024import java.util.Set;
025
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.configuration.DefaultConfiguration;
029import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.Constants;
033import org.apache.commons.lang.ArrayUtils;
034import org.apache.commons.lang.StringUtils;
035
036import org.ametys.core.cache.AbstractCacheManager;
037import org.ametys.core.cache.Cache;
038import org.ametys.runtime.i18n.I18nizableText;
039import org.ametys.runtime.plugin.PluginsManager;
040import org.ametys.runtime.plugin.component.AbstractThreadSafeComponentExtensionPoint;
041
042/**
043 * This class is in charge of handling dynamic content type descriptor extension point.<br>
044 * This point handles the pool of available dynamic content type descriptors.
045 */
046public class DynamicContentTypeDescriptorExtentionPoint extends AbstractThreadSafeComponentExtensionPoint<DynamicContentTypeDescriptor>
047{
048    /** Avalon Role */
049    public static final String ROLE = "org.ametys.cms.contenttype.DynamicContentTypeExtentionPoint"; // FIXME RUNTIME-1023 DynamicContentTypeDescriptorExtentionPoint.class.getName();
050    
051    private static final String DYNAMIC_CONTENT_TYPE_DESCRIPTOR_CACHE = "dynamicContentTypeDescriptorCache";
052    
053    private ServiceManager _smanager;
054    private ContentTypeExtensionPoint _contentTypeEP;
055    private ContentTypesHelper _contentTypesHelper;
056    private AbstractCacheManager _cacheManager;
057    
058    @Override
059    public void service(ServiceManager manager) throws ServiceException
060    {
061        super.service(manager);
062        _smanager = manager;
063        _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
064        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
065    }
066    
067    @Override
068    public void initialize() throws Exception
069    {
070        super.initialize();
071        _cacheManager.createMemoryCache(DYNAMIC_CONTENT_TYPE_DESCRIPTOR_CACHE,
072                new I18nizableText("plugin.cms", "PLUGIN_CMS_CACHE_DYNAMIC_CONTENT_TYPE_DESCRIPTOR_LABEL"),
073                new I18nizableText("plugin.cms", "PLUGIN_CMS_CACHE_DYNAMIC_CONTENT_TYPE_DESCRIPTOR_DESCRIPTION"),
074                true,
075                null);
076    }
077    
078    @Override
079    public void initializeExtensions() throws Exception
080    {
081        org.apache.cocoon.environment.Context cocoonContext = (org.apache.cocoon.environment.Context) _context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
082        final Set<String> pluginNames = PluginsManager.getInstance().getPluginNames();
083        
084        File rootFile = new File(cocoonContext.getRealPath("/WEB-INF/param/content-types/_dynamic"));
085        File[] plugins = rootFile.listFiles(new FilenameFilter()
086        {
087            @Override
088            public boolean accept(File dir, String name)
089            {
090                return pluginNames.contains(name);
091            }
092        });
093        
094        if (plugins != null)
095        {
096            for (File pluginDir : plugins)
097            {
098                String pluginName = pluginDir.getName();
099                
100                File[] files = pluginDir.listFiles(new FilenameFilter()
101                {
102                    @Override
103                    public boolean accept(File dir, String name)
104                    {
105                        return name.endsWith(".xml");
106                    }
107                });
108                
109                for (File file : files)
110                {
111                    Configuration configuration = null;
112                    try (InputStream is = new FileInputStream(file))
113                    {
114                        configuration = new DefaultConfigurationBuilder(true).build(is);
115                        
116                        String id = "dynamic-content-type." + file.getName().substring(0, file.getName().lastIndexOf('.'));
117                        
118                        DefaultConfiguration conf = new DefaultConfiguration("extension");
119                        conf.setAttribute("id", id);
120                        conf.addChild(configuration);
121                        
122                        addComponent(pluginName, "unknown", id, DynamicContentTypeDescriptor.class, conf);
123                    }
124                    catch (Exception ex)
125                    {
126                        throw new ConfigurationException("Unable to parse dynamic content type configuration at WEB-INF/param/content-types/dynamic/" + file.getName() + ".xml", configuration, ex);
127                    }
128                }
129            }
130        }
131        
132        super.initializeExtensions();
133    }
134    
135    /**
136     * Get the matching dynamic descriptor for given content types and mixins
137     * @param contentTypes The content types
138     * @param mixinTypes The mixins
139     * @return The matching dynamic descriptor or <code>null</code> if not found
140     */
141    public DynamicContentTypeDescriptor getMatchingDescriptor (String[] contentTypes, String[] mixinTypes)
142    {
143        String cacheId = _getCacheIdentifier(contentTypes, mixinTypes);
144        
145        String extensionId = _getCache().get(cacheId);
146        if (extensionId != null)
147        {
148            return getExtension(extensionId);
149        }
150        
151        DynamicContentTypeDescriptor matchingDynamicCType = null;
152        
153        int bestCTypeScore = 0;
154        int bestMixinScore = 0;
155        
156        for (String id : getExtensionsIds())
157        {
158            int cTypeScore = 0;
159            int mixinScore = 0;
160            
161            DynamicContentTypeDescriptor dynamicContentType = getExtension(id);
162            
163            boolean dynamicTypesMatch = true;
164            String[] dynamicCTypes = dynamicContentType.getSupertypeIds();
165            Set<String> cTypesAndAncestors = _getContentTypesAndAncestor(contentTypes, mixinTypes);
166            for (String cTypeId : dynamicCTypes)
167            {
168                if (!cTypesAndAncestors.contains(cTypeId))
169                {
170                    // Dynamic types do not include all content types, ignore
171                    dynamicTypesMatch = false;
172                    break;
173                }
174            }
175            
176            if (dynamicTypesMatch)
177            {
178                for (String cTypeId : dynamicContentType.getSupertypeIds())
179                {
180                    ContentType cType = _contentTypeEP.getExtension(cTypeId);
181                    
182                    if (!cType.isMixin() && ArrayUtils.contains(contentTypes, cTypeId))
183                    {
184                        cTypeScore += 1;
185                    }
186                    else if (cType.isMixin() && ArrayUtils.contains(mixinTypes, cTypeId))
187                    {
188                        mixinScore += 1;
189                    }
190                }
191                
192                if (cTypeScore > bestCTypeScore
193                    || cTypeScore == bestCTypeScore && mixinScore > bestMixinScore)
194                {
195                    bestCTypeScore = cTypeScore;
196                    bestMixinScore = mixinScore;
197
198                    matchingDynamicCType = dynamicContentType;
199                    _getCache().put(cacheId, id);
200                }
201            }
202        }
203        
204        return matchingDynamicCType;
205    }
206    
207    private Set<String> _getContentTypesAndAncestor (String[] contentTypes, String[] mixins)
208    {
209        Set<String> cTypesAndAncestors = new HashSet<>();
210        
211        if (_contentTypesHelper == null)
212        {
213            try
214            {
215                _contentTypesHelper = (ContentTypesHelper) _smanager.lookup(ContentTypesHelper.ROLE);
216            }
217            catch (ServiceException e)
218            {
219                throw new IllegalStateException(e);
220            }
221        }
222        
223        String[] allContentTypes = (String[]) ArrayUtils.addAll(contentTypes, mixins);
224        
225        for (String id : allContentTypes)
226        {
227            cTypesAndAncestors.add(id);
228            cTypesAndAncestors.addAll(_contentTypesHelper.getAncestors(id));
229        }
230        
231        return cTypesAndAncestors;
232        
233    }
234    
235    private String _getCacheIdentifier (String[] contentTypes, String[] mixins)
236    {
237        Arrays.sort(contentTypes);
238        String name = StringUtils.join(contentTypes, ";");
239        
240        if (mixins.length > 0)
241        {
242            Arrays.sort(mixins);
243            name += ";";
244            name += StringUtils.join(mixins, ";");
245        }
246        
247        return name;
248    }
249    
250    private Cache<String, String> _getCache()
251    {
252        return _cacheManager.get(DYNAMIC_CONTENT_TYPE_DESCRIPTOR_CACHE);
253    }
254}