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