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