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