001/*
002 *  Copyright 2017 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.odf.export;
017
018import java.io.IOException;
019import java.util.Collection;
020import java.util.Date;
021import java.util.HashMap;
022import java.util.Iterator;
023import java.util.Map;
024import java.util.NoSuchElementException;
025
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.cocoon.ProcessingException;
029import org.apache.cocoon.environment.ObjectModelHelper;
030import org.apache.cocoon.environment.Request;
031import org.apache.cocoon.generation.ServiceableGenerator;
032import org.apache.cocoon.xml.AttributesImpl;
033import org.apache.cocoon.xml.XMLUtils;
034import org.apache.commons.lang3.ArrayUtils;
035import org.apache.commons.lang3.StringUtils;
036import org.xml.sax.SAXException;
037
038import org.ametys.cms.contenttype.ContentTypesHelper;
039import org.ametys.cms.contenttype.MetadataManager;
040import org.ametys.cms.contenttype.MetadataSet;
041import org.ametys.cms.repository.Content;
042import org.ametys.core.util.DateUtils;
043import org.ametys.odf.ODFHelper;
044import org.ametys.odf.ProgramItem;
045import org.ametys.odf.course.Course;
046import org.ametys.odf.enumeration.OdfReferenceTableEntry;
047import org.ametys.odf.enumeration.OdfReferenceTableHelper;
048import org.ametys.odf.program.AbstractProgram;
049import org.ametys.odf.program.Program;
050import org.ametys.odf.tree.OdfClassificationHandler;
051import org.ametys.plugins.repository.AmetysObjectIterable;
052import org.ametys.plugins.repository.AmetysObjectIterator;
053import org.ametys.plugins.repository.AmetysObjectResolver;
054import org.ametys.plugins.repository.AmetysRepositoryException;
055import org.ametys.plugins.repository.UnknownAmetysObjectException;
056import org.ametys.plugins.repository.version.VersionAwareAmetysObject;
057
058/**
059 * Generate the ODF structure with 2 levels (metadata), the catalog and the lang. It's possible to determine a metadataset to sax data.
060 * 
061 * You should call this generator with the following parameters :
062 *  - catalog : identifier of the catalog
063 *  - lang : language code (fr, en, etc.)
064 *  - level1 : name of the metadata for the first level
065 *  - level2 : name of the metadata for the second level
066 *  - metadataSet (optional) : name of the metadata set to sax values
067 * 
068 */
069public class ExportCatalogByLevelsGenerator extends ServiceableGenerator
070{
071    /** The AmetysObject resolver */
072    protected AmetysObjectResolver _resolver;
073    
074    /** The Metadata Manager */
075    protected MetadataManager _metadataManager;
076    
077    /** The content type helper */
078    protected ContentTypesHelper _contentTypesHelper;
079    
080    /** The ODF helper */
081    protected ODFHelper _odfHelper;
082    
083    /** The ODF classification handler */
084    protected OdfClassificationHandler _odfClassificationHandler;
085    
086    /** The helper for reference tables*/
087    protected OdfReferenceTableHelper _odfRefTableHelper;
088    
089    @Override
090    public void service(ServiceManager smanager) throws ServiceException
091    {
092        super.service(smanager);
093        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
094        _metadataManager = (MetadataManager) smanager.lookup(MetadataManager.ROLE);
095        _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
096        _odfHelper = (ODFHelper) smanager.lookup(ODFHelper.ROLE);
097        _odfClassificationHandler = (OdfClassificationHandler) smanager.lookup(OdfClassificationHandler.ROLE);
098        _odfRefTableHelper = (OdfReferenceTableHelper) smanager.lookup(OdfReferenceTableHelper.ROLE);
099    }
100    
101    @Override
102    public void generate() throws IOException, SAXException, ProcessingException
103    {
104        contentHandler.startDocument();
105        XMLUtils.startElement(contentHandler, "Ametys-ODF");
106        
107        /* Test parameters */
108        Map<String, String> wsParameters = new HashMap<>();
109        if (getParameters(wsParameters))
110        {
111            /* Build and SAX ODF Structure */
112            AmetysObjectIterable<Program> programs = getConcernedPrograms(wsParameters);
113            
114            // Switch to version label if not empty
115            String versionLabel = parameters.getParameter("versionLabel", null);
116            if (StringUtils.isNotBlank(versionLabel))
117            {
118                // wrap the current iterable into a iteratable which will filtered programs by version label
119                programs = new FilteredByVersionLabelIterable<>(programs, versionLabel);
120            }
121            
122            Map<String, Map<String, Collection<Program>>> odfStructure =  _odfClassificationHandler.organizeProgramsByLevels(programs, wsParameters.get("level1"), wsParameters.get("level2"));
123
124            String level1 = wsParameters.get("level1");
125            String level2 = wsParameters.get("level2");
126            String metadataSet = wsParameters.get("metadataSet");
127            
128            for (String level1Value : odfStructure.keySet())
129            {
130                AttributesImpl attrs = new AttributesImpl();
131                _addLevelAttributes(attrs, level1Value, wsParameters.get("lang"));
132                XMLUtils.startElement(contentHandler, level1, attrs);
133                
134                Map<String, Collection<Program>> level2Values = odfStructure.get(level1Value);
135                for (String level2Value : level2Values.keySet())
136                {
137                    attrs.clear();
138                    _addLevelAttributes(attrs, level2Value, wsParameters.get("lang"));
139                    
140                    XMLUtils.startElement(contentHandler, level2, attrs);
141                    for (Program program : level2Values.get(level2Value))
142                    {
143                        _saxStructure(program, program, metadataSet, wsParameters);
144                    }
145                    XMLUtils.endElement(contentHandler, level2);
146                }
147                
148                XMLUtils.endElement(contentHandler, level1);
149            }
150        }
151        
152        XMLUtils.endElement(contentHandler, "Ametys-ODF");
153        contentHandler.endDocument();
154    }
155    
156    /**
157     * Add attributes for classification level
158     * @param attrs The XML attributes
159     * @param value The level's value
160     * @param lang The language
161     */
162    protected void _addLevelAttributes(AttributesImpl attrs, String value, String lang)
163    {
164        if (_resolver.hasAmetysObjectForId(value))
165        {
166            Content content = _resolver.resolveById(value);
167            
168            if (_odfRefTableHelper.isTableReferenceEntry(content))
169            {
170                OdfReferenceTableEntry entry = new OdfReferenceTableEntry(content);
171                attrs.addCDATAAttribute("id", entry.getId());
172                attrs.addCDATAAttribute("code", entry.getCode());
173                attrs.addCDATAAttribute("title", entry.getLabel(lang));
174            }
175            else
176            {
177                attrs.addCDATAAttribute("id", content.getId());
178                attrs.addCDATAAttribute("title", content.getTitle());
179            }
180        }
181        else
182        {
183            attrs.addCDATAAttribute("value", value);
184        }
185    }
186    
187    /**
188     * Get the parameters from the request and test it.
189     * @param wsParameters Map of parameters to fill
190     * @return false if a parameter is missing or something going wrong with the parameters, otherwise true
191     * @throws SAXException if an error occured
192     */
193    protected boolean getParameters(Map<String, String> wsParameters) throws SAXException
194    {
195        Request request = ObjectModelHelper.getRequest(objectModel);
196
197        String catalog = getParameter(request, "catalog");
198        String lang = getParameter(request, "lang");
199        String level1 = getParameter(request, "level1");
200        String level2 = getParameter(request, "level2");
201        
202        boolean isValidLevelParameters = true;
203        if (level1 != null && !_odfClassificationHandler.isEligibleMetadataForLevel(level1, true))
204        {
205            XMLUtils.createElement(contentHandler, "error", "The metadata " + level1 + " is not an eligible metadata for the export");
206            isValidLevelParameters = false;
207        }
208        
209        if (level2 != null && !_odfClassificationHandler.isEligibleMetadataForLevel(level2, true))
210        {
211            XMLUtils.createElement(contentHandler, "error", "The metadata " + level2 + " is not an eligible metadata for the export");
212            isValidLevelParameters = false;
213        }
214        
215        wsParameters.put("catalog", catalog);
216        wsParameters.put("lang", lang);
217        wsParameters.put("level1", level1);
218        wsParameters.put("level2", level2);
219        
220        String metadataSet = request.getParameter("metadataSet");
221        if (StringUtils.isBlank(metadataSet))
222        {
223            metadataSet = "main";
224        }
225        wsParameters.put("metadataSet", metadataSet);
226        
227        return catalog != null && lang != null && isValidLevelParameters && level1 != null && level2 != null;
228    }
229    
230    /**
231     * Get the parameter from the request and test if it's not null or blank.
232     * Sax an error if the parameter is missing or empty.
233     * @param request The request
234     * @param parameterName The parameter name
235     * @return null when the parameter is missing or empty, otherwise the parameter value
236     * @throws SAXException if an error occured
237     */
238    protected String getParameter(Request request, String parameterName) throws SAXException
239    {
240        String parameterValue = parameters.getParameter(parameterName, request.getParameter(parameterName));
241
242        if (StringUtils.isBlank(parameterValue))
243        {
244            XMLUtils.createElement(contentHandler, "error", "Missing parameter (cannot be empty) : " + parameterName);
245            parameterValue = null;
246        }
247        
248        return parameterValue;
249    }
250    
251    /**
252     * Get the programs to SAX.
253     * @param wsParameters Parameters of the web service
254     * @return A Collection of programs
255     */
256    protected AmetysObjectIterable<Program> getConcernedPrograms(Map<String, String> wsParameters)
257    {
258        return _odfClassificationHandler.getPrograms(wsParameters.get("catalog"), wsParameters.get("lang"), wsParameters.get("level1"), null, wsParameters.get("level2"), null, null, null, null);
259    }
260    
261    /**
262     * Sax the structure of the parentProgram by exploring its children and saxing metadata containing into the passed metadataSet.
263     * @param parentProgram Initial program
264     * @param programItem Part of the program to explore
265     * @param metadataSetName Name of the metadata set to SAX
266     * @param wsParameters Parameters of the web service
267     * @throws AmetysRepositoryException if an error occured
268     * @throws SAXException if an error occured
269     * @throws IOException if an error occured
270     */
271    private void _saxStructure(Program parentProgram, ProgramItem programItem, String metadataSetName, Map<String, String> wsParameters) throws AmetysRepositoryException, SAXException, IOException
272    {
273        if (programItem instanceof AbstractProgram || programItem instanceof Course)
274        {
275            Content content = (Content) programItem;
276            
277            String contentType = content.getTypes()[0];
278            contentType = contentType.substring(contentType.lastIndexOf(".") + 1);
279
280            AttributesImpl attrs = getContentAttributes(programItem, parentProgram, wsParameters);
281            
282            XMLUtils.startElement(contentHandler, contentType, attrs);
283    
284            /* SAX metadata for flat level */
285            MetadataSet metadataSet = _contentTypesHelper.getMetadataSetForView(metadataSetName, content.getTypes(), content.getMixinTypes());
286            _metadataManager.saxMetadata(contentHandler, content, metadataSet, null);
287            
288            /* SAX structure of the ProgramItem */
289            _saxChildren(parentProgram, programItem, metadataSetName, wsParameters);
290            
291            XMLUtils.endElement(contentHandler, contentType);
292        }
293        else
294        {
295            /* SAX structure of the ProgramItem */
296            _saxChildren(parentProgram, programItem, metadataSetName, wsParameters);
297        }
298    }
299    
300    /**
301     * Explore and sax children of the passed program item.
302     * @param parentProgram Initial program
303     * @param programItem Part of the program to explore
304     * @param metadataSetName Name of the metadata set to SAX
305     * @param wsParameters Parameters of the web service
306     * @throws AmetysRepositoryException if an error occured
307     * @throws SAXException if an error occured
308     * @throws IOException if an error occured
309     */
310    private void _saxChildren(Program parentProgram, ProgramItem programItem, String metadataSetName, Map<String, String> wsParameters) throws AmetysRepositoryException, SAXException, IOException
311    {
312        for (ProgramItem child : _odfHelper.getChildProgramItems(programItem))
313        {
314            _saxStructure(parentProgram, child, metadataSetName, wsParameters);
315        }
316    }
317    
318    /**
319     * Get attributes for the current saxed content (title, id, etc.).
320     * @param programItem Part of the program to get attributes
321     * @param parentProgram Initial program
322     * @param wsParameters Parameters of the web service
323     * @return The attributes to sax
324     */
325    protected AttributesImpl getContentAttributes(ProgramItem programItem, Program parentProgram, Map<String, String> wsParameters)
326    {
327        Content content = (Content) programItem;
328        
329        AttributesImpl attrs = new AttributesImpl();
330        attrs.addCDATAAttribute("title", content.getTitle());
331        attrs.addCDATAAttribute("id", content.getId());
332        Date lastValidated = content.getLastValidationDate();
333        if (lastValidated != null)
334        {
335            attrs.addCDATAAttribute("lastValidated", DateUtils.dateToString(lastValidated));
336        }
337        return attrs;
338    }
339    
340    class FilteredByVersionLabelIterable<P extends VersionAwareAmetysObject> implements AmetysObjectIterable<P>
341    {
342        private AmetysObjectIterable<P> _initialIterable;
343        private String _versionLabel;
344        
345        /**
346         * Creates a {@link AmetysObjectIterable} which will filter and get elements with given version label
347         * @param it the initial {@link AmetysObjectIterable}s
348         * @param versionLabel The version label to filter by
349         */
350        public FilteredByVersionLabelIterable(AmetysObjectIterable<P> it, String versionLabel)
351        {
352            _initialIterable = it;
353            _versionLabel = versionLabel;
354        }
355        
356        public long getSize()
357        {
358            return -1;
359        }
360        
361        public AmetysObjectIterator<P> iterator()
362        {
363            return new FilteredByVersionLabelIterator(_initialIterable.iterator(), _initialIterable.getSize(), _versionLabel);
364        }
365        
366        public void close()
367        {
368            // nothing to do
369        }
370        
371        class FilteredByVersionLabelIterator implements AmetysObjectIterator<P>
372        {
373            private long _invalids;
374            private Iterator<P> _it;
375            private int _pos;
376            private long _size;
377            private P _nextObject;
378            private String _label;
379            
380            public FilteredByVersionLabelIterator(Iterator<P> it, long size, String label)
381            {
382                _it = it;
383                _size = size;
384                _invalids = 0;
385                _label = label;
386            }
387            
388            public boolean hasNext()
389            {
390                // Prefetch the next object
391                if (_nextObject == null)
392                {
393                    while (_it.hasNext())
394                    {
395                        P next = _it.next();
396                        try
397                        {
398                            String[] allLabels = next.getAllLabels();
399                            if (ArrayUtils.contains(allLabels, _label))
400                            {
401                                next.switchToLabel(_label);
402                                _nextObject = next;
403                                return true;
404                            }
405                            else
406                            {
407                                // Go to next element
408                                _invalids++;
409                            }
410                        }
411                        catch (UnknownAmetysObjectException e)
412                        {
413                            // Go to next element
414                            _invalids++;
415                        }
416                    }
417                    return false;
418                }
419                return true;
420            }
421
422            public P next()
423            {
424                if (!hasNext())
425                {
426                    throw new NoSuchElementException();
427                }
428                
429                try
430                {
431                    _pos++;
432                    return _nextObject;
433                }
434                finally
435                {
436                    _nextObject = null;
437                }
438            }
439
440            public long getSize()
441            {
442                return _size - _invalids;
443            }
444            
445            public long getPosition()
446            {
447                return _pos;
448            }
449        }
450    }
451}