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