001/*
002 *  Copyright 2012 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.plugins.odfweb.repository;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Comparator;
022import java.util.List;
023import java.util.Map;
024import java.util.Objects;
025import java.util.Optional;
026import java.util.Set;
027import java.util.TreeMap;
028
029import org.apache.avalon.framework.activity.Initializable;
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034import org.apache.commons.lang3.StringUtils;
035
036import org.ametys.cms.repository.ContentTypeExpression;
037import org.ametys.cms.repository.LanguageExpression;
038import org.ametys.core.cache.AbstractCacheManager;
039import org.ametys.core.cache.Cache;
040import org.ametys.odf.ProgramItem;
041import org.ametys.odf.program.Program;
042import org.ametys.odf.program.ProgramFactory;
043import org.ametys.odf.tree.OdfClassificationHandler.LevelValue;
044import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
045import org.ametys.plugins.repository.AmetysObject;
046import org.ametys.plugins.repository.AmetysObjectFactoryExtensionPoint;
047import org.ametys.plugins.repository.AmetysObjectIterable;
048import org.ametys.plugins.repository.AmetysObjectResolver;
049import org.ametys.plugins.repository.CollectionIterable;
050import org.ametys.plugins.repository.UnknownAmetysObjectException;
051import org.ametys.plugins.repository.provider.WorkspaceSelector;
052import org.ametys.plugins.repository.query.QueryHelper;
053import org.ametys.plugins.repository.query.expression.AndExpression;
054import org.ametys.plugins.repository.query.expression.Expression.Operator;
055import org.ametys.plugins.repository.query.expression.StringExpression;
056import org.ametys.runtime.i18n.I18nizableText;
057import org.ametys.runtime.plugin.component.AbstractLogEnabled;
058import org.ametys.web.repository.page.Page;
059
060import com.google.common.collect.Iterables;
061
062/**
063 * Maintains a per-request cache, dispatching ODF virtual pages accross degrees and domains.
064 */
065public class ODFPageCache extends AbstractLogEnabled implements Serviceable, Initializable, Component
066{
067    /** Avalon role. */
068    public static final String ROLE = ODFPageCache.class.getName();
069    
070    // Tree key when no first level metadata has been selected
071    private static final String __NO_FIRST_DATA_KEY = "_no-first-data";
072    
073    // Tree key when no second level metadata has been selected
074    private static final String __NO_SECOND_DATA_KEY = "_no-second-data";
075    
076    private static final String __TREE_CACHE = ODFPageCache.class.getName() + "$tree";
077    private static final String __PROGRAM_CACHE = ODFPageCache.class.getName() + "$program";
078    private static final String __INDEXABLE_CACHE = ODFPageCache.class.getName() + "$indexable";
079    
080    private OdfPageHandler _odfPageHandler;
081    private WorkspaceSelector _workspaceSelector;
082    private AmetysObjectResolver _resolver;
083
084    private AmetysObjectFactoryExtensionPoint _ametysObjectFactoryEP;
085
086    private AbstractCacheManager _cacheManager;
087    
088    @Override
089    public void service(ServiceManager manager) throws ServiceException
090    {
091        _odfPageHandler = (OdfPageHandler) manager.lookup(OdfPageHandler.ROLE);
092        _workspaceSelector = (WorkspaceSelector) manager.lookup(WorkspaceSelector.ROLE);
093        _ametysObjectFactoryEP = (AmetysObjectFactoryExtensionPoint) manager.lookup(AmetysObjectFactoryExtensionPoint.ROLE);
094        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
095        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
096    }
097    
098    public void initialize() throws Exception
099    {
100        _cacheManager.createRequestCache(__TREE_CACHE,
101                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_PAGE_TREE_LABEL"),
102                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_PAGE_TREE_DESCRIPTION"),
103                false);
104        _cacheManager.createRequestCache(__PROGRAM_CACHE,
105                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_PAGE_PROGRAM_LABEL"),
106                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_PAGE_PROGRAM_DESCRIPTION"),
107                false);
108        _cacheManager.createRequestCache(__INDEXABLE_CACHE,
109                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_ROOT_INDEXABLE_LABEL"),
110                new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_ROOT_INDEXABLE_DESCRIPTION"),
111                false);
112    }
113    
114    Map<String, Map<String, Collection<Program>>> getProgramCache(Page rootPage, boolean computeIfNotPresent)
115    {
116        String workspace = _workspaceSelector.getWorkspace();
117        String pageId = rootPage.getId();
118        
119        Cache<ODFPageCacheKey, Map<String, Map<String, Collection<Program>>>> odfPageCache = _getODFPageCache();
120        Map<String, Map<String, Collection<Program>>> level1Tree = odfPageCache.get(ODFPageCacheKey.of(workspace, pageId));
121        
122        if (level1Tree == null)
123        {
124            if (!computeIfNotPresent)
125            {
126                return null;
127            }
128            
129            Map<String, LevelValue> level1Values = _odfPageHandler.getLevel1Values(rootPage);
130            Set<String> level1Codes = level1Values.keySet();
131            
132            LevelComparator level1Comparator = new LevelComparator(level1Values);
133            
134            Map<String, LevelValue> level2Values = _odfPageHandler.getLevel2Values(rootPage);
135            Set<String> level2Codes = level2Values.keySet();
136            LevelComparator level2Comparator = new LevelComparator(level2Values);
137            
138            level1Tree = new TreeMap<>(level1Comparator);
139            odfPageCache.put(ODFPageCacheKey.of(workspace, pageId), level1Tree);
140            
141            AmetysObjectIterable<Program> programs = _odfPageHandler.getProgramsWithRestrictions(rootPage, null, null, null, null);
142            
143            String level1MetaPath = _odfPageHandler.getLevel1Metadata(rootPage);
144            String level2MetaPath = _odfPageHandler.getLevel2Metadata(rootPage);
145            
146            for (Program program : programs)
147            {
148                if (StringUtils.isBlank(level1MetaPath))
149                {
150                    Map<String, Collection<Program>> level2Tree = level1Tree.computeIfAbsent(__NO_FIRST_DATA_KEY, x -> new TreeMap<>(level2Comparator));
151                    Collection<Program> programCache = level2Tree.computeIfAbsent(__NO_SECOND_DATA_KEY, x -> new ArrayList<>());
152                    
153                    programCache.add(program);
154                }
155                else if (StringUtils.isBlank(level2MetaPath))
156                {
157                    String programL1Value = _odfPageHandler.getProgramLevelValue(program, level1MetaPath);
158                    if (level1Codes.contains(programL1Value))
159                    {
160                        Map<String, Collection<Program>> level2Tree = level1Tree.computeIfAbsent(programL1Value, x -> new TreeMap<>(level2Comparator));
161                        Collection<Program> programCache = level2Tree.computeIfAbsent(__NO_SECOND_DATA_KEY, x -> new ArrayList<>());
162                        
163                        programCache.add(program);
164                    }
165                }
166                else
167                {
168                    String programL1Value = _odfPageHandler.getProgramLevelValue(program, level1MetaPath);
169                    String programL2Value = _odfPageHandler.getProgramLevelValue(program, level2MetaPath);
170                    
171                    if (level1Codes.contains(programL1Value) && level2Codes.contains(programL2Value))
172                    {
173                        Map<String, Collection<Program>> level2Tree = level1Tree.computeIfAbsent(programL1Value, x -> new TreeMap<>(level2Comparator));
174                        Collection<Program> programCache = level2Tree.computeIfAbsent(programL2Value, x -> new ArrayList<>());
175                        
176                        programCache.add(program);
177                    }
178                }
179            }
180        }
181        
182        return level1Tree;
183    }
184    
185    boolean areChildrenIndexable(Page rootPage)
186    {
187        return _getODFRootIndexablePageCache().get(rootPage.getId(), __ -> rootPage.getValue(AbstractOdfPage.INDEXABLE_CHILDREN, true));
188    }
189
190    private static class LevelComparator implements Comparator<String>
191    {
192        private final Map<String, LevelValue> _levelValues;
193        
194        public LevelComparator(Map<String, LevelValue> initialMap)
195        {
196            _levelValues = initialMap;
197        }
198        
199        public int compare(String s1, String s2)
200        {
201            if (_levelValues.containsKey(s1))
202            {
203                // Compare levels on orders then labels
204                return _levelValues.get(s1).compareTo(_levelValues.get(s2));
205            }
206            else
207            {
208                // _no-first-data or _no-second-data
209                return 0;
210            }
211        }
212    }
213    
214    /**
215     * Get programs to given levels
216     * @param rootPage The ODF root page
217     * @param level1 The value of first level or <code>null</code> if there is no first level
218     * @param level2 The value of second level or <code>null</code> if there is no second level
219     * @param computeIfNotPresent When false, no result will be returned if the root page in not already in the cache
220     * @return The matching programs
221     */
222    Optional<AmetysObjectIterable<Program>> getPrograms(Page rootPage, String level1, String level2, boolean computeIfNotPresent)
223    {
224        return Optional.ofNullable(getProgramCache(rootPage, computeIfNotPresent))
225                .map(firstLevelCache -> firstLevelCache.get(Optional.ofNullable(level1).orElse(__NO_FIRST_DATA_KEY)))
226                .map(secondLevelCache -> secondLevelCache.get(Optional.ofNullable(level2).orElse(__NO_SECOND_DATA_KEY)))
227                .map(CollectionIterable<Program>::new);
228    }
229    
230    /**
231     * Get the child page from its relative path.
232     * @param rootPage The ODF root page
233     * @param parentPage The parent page
234     * @param level1 The value of first level or <code>null</code> if there is no first level
235     * @param level2 The value of second level or <code>null</code> if there is no second level
236     * @param path the path of the child page
237     * @return the child page
238     * @throws UnknownAmetysObjectException if no child page was found at the given path
239     */
240    Page getChildProgramPage(Page rootPage, Page parentPage, String level1, String level2, String path) throws UnknownAmetysObjectException
241    {
242        List<String> headQueuePath = Arrays.asList(StringUtils.split(path, "/", 2));
243        String queuePath = Iterables.get(headQueuePath, 1, null);
244        
245        String pageName = headQueuePath.get(0);
246
247        ProgramPageFactory programPageFactory = (ProgramPageFactory) _ametysObjectFactoryEP.getExtension(ProgramPageFactory.class.getName());
248        
249        return getProgramFromPageName(rootPage, level1, level2, pageName)
250            .map(program -> {
251                return programPageFactory.createProgramPage(rootPage, program, null, null, parentPage);
252            })
253            .map(cp -> _odfPageHandler.addRedirectIfNeeded(cp, pageName))
254            .map(cp -> _odfPageHandler.exploreQueuePath(cp, queuePath))
255            .orElseThrow(() -> new UnknownAmetysObjectException("There's no program for page's name " + pageName));
256    }
257    
258    /**
259     * Get cached program corresponding to the page name
260     * @param rootPage The ODF root page
261     * @param level1 The value of first level or <code>null</code> if there is no first level
262     * @param level2 The value of second level or <code>null</code> if there is no second level
263     * @param pageName the page's name
264     * @return The program if found
265     */
266    Optional<Program> getProgramFromPageName(Page rootPage, String level1, String level2, String pageName)
267    {
268        // Page's name is like [title]-[code]
269        String programName = null;
270        String programCode = null;
271        
272        // FIXME Search for program's name for legacy purpose. To be removed in a later version.
273        // In legacy, page's name can be like the program name (program-[title])
274        int j = pageName.lastIndexOf("program-");
275        if (j != -1)
276        {
277            String name = pageName.substring(j);
278            // The page name contains "program-", so it could be a legacy page name. 
279            // ... but it also could be a program whose title contains the text "program"
280            // ... so check if the program name exist to assure it a legacy page name
281            if (_doesProgramNameExist(rootPage, name))
282            {
283                programName = name;
284            }
285        }
286        
287        if (programName == null)
288        {
289            j = pageName.lastIndexOf("-");
290            programCode = j == -1 ? pageName : pageName.substring(j + 1);
291        }
292        
293        return Optional.ofNullable(getProgram(rootPage, level1, level2, programCode, programName));
294    }
295    
296    private boolean _doesProgramNameExist(Page rootPage, String programName)
297    {
298        String catalog = _odfPageHandler.getCatalog(rootPage);
299        String lang = rootPage.getSitemapName();
300        
301        AndExpression expression = new AndExpression();
302        expression.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE));
303        expression.add(new LanguageExpression(Operator.EQ, lang));
304        if (catalog != null)
305        {
306            expression.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog));
307        }
308        
309        String xPathQuery = QueryHelper.getXPathQuery(programName, "ametys:content", expression);
310        AmetysObjectIterable<AmetysObject> programs = _resolver.query(xPathQuery);
311        return programs.getSize() > 0;
312    }
313    
314    /**
315     * Get cached program
316     * @param rootPage The ODF root page
317     * @param level1 The value of first level or <code>null</code> if there is no first level
318     * @param level2 The value of second level or <code>null</code> if there is no second level
319     * @param programCode The code of program. Can be null if programName is not null.
320     * @param programName The name of program. Can be null if programCode is not null.
321     * @return program The program
322     */
323    Program getProgram(Page rootPage, String level1, String level2, String programCode, String programName)
324    {
325        String workspace = _workspaceSelector.getWorkspace();
326        String pageId = rootPage.getId();
327        
328        // Map<level1, Map<level2, Map<program name, Program>>>
329        Cache<ODFPageProgramCacheKey, Program> odfPageCache = _getODFPageProgramCache();
330        
331        String level1Key = Objects.toString(level1, __NO_FIRST_DATA_KEY);
332        String level2Key = Objects.toString(level2, __NO_SECOND_DATA_KEY);
333        
334        // For legacy purpose we use the programName when the programCode is null.
335        String programKey = Objects.toString(programCode, programName);
336        
337        return odfPageCache.get(
338            ODFPageProgramCacheKey.of(workspace, pageId, level1Key, level2Key, programKey),
339            key -> this._getProgramWithRestrictions(rootPage, level1, level2, programCode, programName)
340        );
341    }
342    
343    private Program _getProgramWithRestrictions(Page rootPage, String level1, String level2, String programCode, String programName)
344    {
345        return _odfPageHandler.getProgramsWithRestrictions(rootPage, level1, level2, programCode, programName).stream()
346                .findFirst().orElse(null);
347    }
348    
349    /**
350     * Clear page cache
351     * @param rootPage The ODF root page
352     */
353    public void clearCache (Page rootPage)
354    {
355        _getODFPageCache().invalidate(ODFPageCacheKey.of(null, rootPage.getId()));
356        _getODFPageProgramCache().invalidate(ODFPageProgramCacheKey.of(null, rootPage.getId(), null, null, null));
357        _getODFRootIndexablePageCache().invalidate(rootPage.getId());
358    }
359    
360    private Cache<ODFPageCacheKey, Map<String, Map<String, Collection<Program>>>> _getODFPageCache()
361    {
362        return _cacheManager.get(__TREE_CACHE);
363    }
364    
365    private static class ODFPageCacheKey extends AbstractCacheKey
366    {
367        public ODFPageCacheKey(String workspace, String pageId)
368        {
369            super(workspace, pageId);
370        }
371        
372        public static ODFPageCacheKey of(String workspace, String pageId)
373        {
374            return new ODFPageCacheKey(workspace, pageId);
375        }
376    }
377    
378    private Cache<ODFPageProgramCacheKey, Program> _getODFPageProgramCache()
379    {
380        return _cacheManager.get(__PROGRAM_CACHE);
381    }
382    
383    private static class ODFPageProgramCacheKey extends AbstractCacheKey
384    {
385        public ODFPageProgramCacheKey(String workspace, String pageId, String level1Value, String level2Value, String programName)
386        {
387            super(workspace, pageId, level1Value, level2Value, programName);
388        }
389        
390        public static ODFPageProgramCacheKey of(String workspace, String pageId, String level1Value, String level2Value, String programName)
391        {
392            return new ODFPageProgramCacheKey(workspace, pageId, level1Value, level2Value, programName);
393        }
394    }
395    
396    private Cache<String, Boolean> _getODFRootIndexablePageCache()
397    {
398        return _cacheManager.get(__INDEXABLE_CACHE);
399    }
400}