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        Optional<ProgramPage> programPage = getProgramFromPageName(rootPage, level1, level2, pageName)
225                .<ProgramPage>map(program -> new ProgramPage((ProgramPageFactory) _ametysObjectFactoryEP.getExtension(ProgramPageFactory.class.getName()), rootPage, program, null, null, parentPage));
226        
227        if (programPage.isPresent())
228        {
229            boolean isLegacyPattern = pageName.lastIndexOf("subprogram-") != -1 || pageName.lastIndexOf("program-") != -1;
230            if (StringUtils.isEmpty(queuePath))
231            {
232                if (isLegacyPattern)
233                {
234                    // FIXME Support of legacy pattern with the program's name (instead of program's code). To be removed in a later version.
235                    getLogger().warn("Redirect legacy path '{}' to '{}' page", parentPage.getPathInSitemap() + '/' + path, programPage.get().getPathInSitemap());
236                    return new RedirectPage(programPage.get());
237                }
238                else
239                {
240                    return programPage.get();
241                }
242            }
243            else
244            {
245                Page child = programPage.get().getChild(queuePath);
246                if (isLegacyPattern)
247                {
248                    // FIXME Support of legacy pattern with the program's name (instead of program's code). To be removed in a later version.
249                    getLogger().warn("Redirect legacy path '{}' to '{}' page", parentPage.getPathInSitemap() + '/' + path, programPage.get().getPathInSitemap() + '/' + queuePath);
250                    return new RedirectPage(child instanceof RedirectPage ? ((RedirectPage) child).getRedirectPage() : child);
251                }
252                else
253                {
254                    return child;
255                }
256            }
257        }
258        else
259        {
260            throw new UnknownAmetysObjectException("There's no program for page's name " + pageName);
261        }
262    }
263    
264    /**
265     * Get cached program corresponding to the page name
266     * @param rootPage The ODF root page
267     * @param level1 The value of first level or <code>null</code> if there is no first level
268     * @param level2 The value of second level or <code>null</code> if there is no second level
269     * @param pageName the page's name
270     * @return The program if found
271     */
272    Optional<Program> getProgramFromPageName(Page rootPage, String level1, String level2, String pageName)
273    {
274        // Page's name is like "title-code"
275        
276        // FIXME Search for program's name for legacy purpose. To be removed in a later version.
277        int j = pageName.lastIndexOf("program-");
278        String programName = j != -1 ? pageName.substring(j) : null;
279        
280        String programCode = null;
281        if (programName == null)
282        {
283            j = pageName.lastIndexOf("-");
284            programCode = j == -1 ? pageName : pageName.substring(j + 1);
285        }
286        
287        return Optional.ofNullable(getProgram(rootPage, level1, level2, programCode, programName));
288    }
289    
290    /**
291     * Get cached program
292     * @param rootPage The ODF root page
293     * @param level1 The value of first level or <code>null</code> if there is no first level
294     * @param level2 The value of second level or <code>null</code> if there is no second level
295     * @param programCode The code of program. Can be null if programName is not null.
296     * @param programName The name of program. Can be null if programCode is not null.
297     * @return program The program
298     */
299    Program getProgram(Page rootPage, String level1, String level2, String programCode, String programName)
300    {
301        String workspace = _workspaceSelector.getWorkspace();
302        String pageId = rootPage.getId();
303        
304        // Map<level1, Map<level2, Map<program name, Program>>>
305        Cache<ODFPageProgramCacheKey, Program> odfPageCache = _getODFPageProgramCache();
306        
307        String level1Key = StringUtils.defaultString(level1, __NO_FIRST_DATA_KEY);
308        String level2Key = StringUtils.defaultString(level2, __NO_SECOND_DATA_KEY);
309        
310        // For legacy purpose we use the programName when the programCode is null.
311        String programKey = StringUtils.defaultString(programCode, programName);
312        
313        return odfPageCache.get(
314            ODFPageProgramCacheKey.of(workspace, pageId, level1Key, level2Key, programKey),
315            key -> this._getProgramWithRestrictions(rootPage, level1, level2, programCode, programName)
316        );
317        
318    }
319    
320    private Program _getProgramWithRestrictions(Page rootPage, String level1, String level2, String programCode, String programName)
321    {
322        return _odfPageHandler.getProgramsWithRestrictions(rootPage, level1, level2, programCode, programName).stream()
323                .findFirst().orElse(null);
324        
325    }
326    
327    /**
328     * Clear page cache
329     * @param rootPage The ODF root page
330     */
331    public void clearCache (Page rootPage)
332    {
333        _getODFPageCache().invalidate(ODFPageCacheKey.of(null, rootPage.getId()));
334        _getODFPageProgramCache().invalidate(ODFPageProgramCacheKey.of(null, rootPage.getId(), null, null, null));
335    }
336    
337    private Cache<ODFPageCacheKey, Map<String, Map<String, Collection<Program>>>> _getODFPageCache()
338    {
339        return _cacheManager.get(__TREE_CACHE);
340    }
341    
342    private static class ODFPageCacheKey extends AbstractCacheKey
343    {
344        public ODFPageCacheKey(String workspace, String pageId)
345        {
346            super(workspace, pageId);
347        }
348        
349        public static ODFPageCacheKey of(String workspace, String pageId)
350        {
351            return new ODFPageCacheKey(workspace, pageId);
352        }
353    }
354    
355    private Cache<ODFPageProgramCacheKey, Program> _getODFPageProgramCache()
356    {
357        return _cacheManager.get(__PROGRAM_CACHE);
358    }
359    
360    private static class ODFPageProgramCacheKey extends AbstractCacheKey
361    {
362        public ODFPageProgramCacheKey(String workspace, String pageId, String level1Value, String level2Value, String programName)
363        {
364            super(workspace, pageId, level1Value, level2Value, programName);
365        }
366        
367        public static ODFPageProgramCacheKey of(String workspace, String pageId, String level1Value, String level2Value, String programName)
368        {
369            return new ODFPageProgramCacheKey(workspace, pageId, level1Value, level2Value, programName);
370        }
371    }
372}