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