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