001/*
002 *  Copyright 2010 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.repository.metadata;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.util.ArrayList;
021import java.util.Date;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Locale;
025import java.util.Map;
026import java.util.Set;
027
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.context.Context;
030import org.apache.avalon.framework.context.ContextException;
031import org.apache.avalon.framework.context.Contextualizable;
032import org.apache.avalon.framework.logger.AbstractLogEnabled;
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.xml.AttributesImpl;
037import org.apache.cocoon.xml.XMLUtils;
038import org.apache.commons.io.IOUtils;
039import org.apache.commons.lang.StringUtils;
040import org.apache.commons.lang3.LocaleUtils;
041import org.xml.sax.ContentHandler;
042import org.xml.sax.SAXException;
043
044import org.ametys.core.user.User;
045import org.ametys.core.user.UserIdentity;
046import org.ametys.core.user.UserManager;
047import org.ametys.core.util.JSONUtils;
048import org.ametys.plugins.repository.AmetysObject;
049import org.ametys.plugins.repository.AmetysObjectIterable;
050import org.ametys.plugins.repository.AmetysObjectResolver;
051import org.ametys.plugins.repository.AmetysRepositoryException;
052import org.ametys.plugins.repository.TraversableAmetysObject;
053import org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType;
054import org.ametys.runtime.i18n.I18nizableText;
055import org.ametys.runtime.parameter.Enumerator;
056import org.ametys.runtime.parameter.ParameterHelper;
057
058/**
059 * Component for helping SAXing metadata.
060 */
061public class MetadataSaxer extends AbstractLogEnabled implements Component, Serviceable, Contextualizable
062{
063    /** The Avalon Role. */
064    public static final String ROLE = MetadataSaxer.class.getName();
065    
066    /** The JSON conversion utilities. */
067    protected JSONUtils _jsonUtils;
068    /** Ametys object resolver */
069    protected AmetysObjectResolver _resolver;
070    /** The avalon context. */
071    protected Context _context;
072    /** The user manager */
073    protected UserManager _userManager;
074    
075    @Override
076    public void contextualize(Context context) throws ContextException
077    {
078        _context = context;
079    }
080    
081    @Override
082    public void service(ServiceManager manager) throws ServiceException
083    {
084        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
085        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
086        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
087    }
088    
089    /**
090     * SAXes a composite metadata.
091     * @param contentHandler the content handler where to SAX into.
092     * @param compositeMetadata the composite metadata.
093     * @throws AmetysRepositoryException if an error occurs.
094     * @throws SAXException if an error occurs.
095     * @throws IOException if an error occurs.
096     */
097    public void saxMetadata(ContentHandler contentHandler, CompositeMetadata compositeMetadata) throws AmetysRepositoryException, SAXException, IOException
098    {
099        _saxAllMetadata(contentHandler, compositeMetadata, "", null);
100    }
101    
102    /**
103     * SAXes a composite metadata.
104     * @param contentHandler the content handler where to SAX into.
105     * @param compositeMetadata the composite metadata.
106     * @param locale The locale to use for localized metadata, such as {@link MultilingualString}. Can be null to sax all existing locales.
107     * @throws AmetysRepositoryException if an error occurs.
108     * @throws SAXException if an error occurs.
109     * @throws IOException if an error occurs.
110     */
111    public void saxMetadata(ContentHandler contentHandler, CompositeMetadata compositeMetadata, Locale locale) throws AmetysRepositoryException, SAXException, IOException
112    {
113        _saxAllMetadata(contentHandler, compositeMetadata, "", locale);
114    }
115    
116    /**
117     * SAX all the metadata of a given composite metadata, with the given prefix.
118     * @param contentHandler the content handler where to SAX into.
119     * @param metadata the composite metadata.
120     * @param prefix the metadata path prefix.
121     * @param locale The locale to use for localized metadata, such as {@link MultilingualString}. Can be null to sax all existing locales.
122     * @throws AmetysRepositoryException if an error occurs.
123     * @throws SAXException if an error occurs.
124     * @throws IOException if an error occurs.
125     */
126    protected void _saxAllMetadata(ContentHandler contentHandler, CompositeMetadata metadata, String prefix, Locale locale) throws AmetysRepositoryException, SAXException, IOException
127    {
128        for (String metadataName : metadata.getMetadataNames())
129        {
130            if (metadata.hasMetadata(metadataName))
131            {
132                _saxMetadata(contentHandler, metadata, metadataName, prefix, locale);
133            }
134        }
135    }
136    
137    /**
138     * SAX a single metadata.
139     * @param contentHandler the content handler where to SAX into.
140     * @param parentMetadata the parent composite metadata.
141     * @param metadataName the metadata name.
142     * @param prefix the metadata path prefix.
143     * @param locale The locale to use for localized metadata, such as {@link MultilingualString}. Can be null to sax all existing locales.
144     * @throws AmetysRepositoryException if an error occurs.
145     * @throws SAXException if an error occurs.
146     * @throws IOException if an error occurs.
147     */
148    protected void _saxMetadata(ContentHandler contentHandler, CompositeMetadata parentMetadata, String metadataName, String prefix, Locale locale) throws AmetysRepositoryException, SAXException, IOException
149    {
150        if (parentMetadata.hasMetadata(metadataName))
151        {
152            MetadataType metadataType = parentMetadata.getType(metadataName);
153            
154            switch (metadataType)
155            {
156                case COMPOSITE:
157                    CompositeMetadata subMetadata = parentMetadata.getCompositeMetadata(metadataName);
158                    
159                    XMLUtils.startElement(contentHandler, metadataName);
160                    
161                    // SAX all metadata contains in the current CompositeMetadata.
162                    _saxAllMetadata(contentHandler, subMetadata, prefix + metadataName + "/", locale);
163                    
164                    XMLUtils.endElement(contentHandler, metadataName);
165                    break;
166                case USER:
167                    _saxUserMetadata(contentHandler, parentMetadata, metadataName);
168                    break;
169                case BINARY:
170                    _saxBinaryMetadata(contentHandler, parentMetadata, metadataName, prefix);
171                    break;
172                    
173                case RICHTEXT:
174                    _saxRichTextMetadata(contentHandler, parentMetadata, metadataName, prefix);
175                    break;
176                    
177                case DATE:
178                    _saxDateMetadata(contentHandler, parentMetadata, metadataName, prefix);
179                    break;
180                    
181                case OBJECT_COLLECTION:
182                    TraversableAmetysObject objectCollection = parentMetadata.getObjectCollection(metadataName);
183                    
184                    XMLUtils.startElement(contentHandler, metadataName);
185                    
186                    // SAX all sub-objects.
187                    for (AmetysObject subObject : objectCollection.getChildren())
188                    {
189                        _saxObject(contentHandler, subObject, prefix + metadataName + "/", false, locale);
190                    }
191                    
192                    XMLUtils.endElement(contentHandler, metadataName);
193                    break;
194                    
195                case MULTILINGUAL_STRING:
196                    _saxMultilingualStringMetadata(contentHandler, parentMetadata, metadataName, prefix, locale);
197                    break;
198                default:
199                    _saxStringMetadata(contentHandler, parentMetadata, metadataName, prefix);
200                    break;
201            }
202        }
203    }
204    
205    /**
206     * SAX a binary metadata.
207     * @param contentHandler the content handler where to SAX into.
208     * @param parentMetadata the parent composite metadata.
209     * @param metadataName the metadata name.
210     * @param prefix the metadata path prefix.
211     * @throws AmetysRepositoryException if an error occurs.
212     * @throws SAXException if an error occurs.
213     */
214    protected void _saxBinaryMetadata(ContentHandler contentHandler, CompositeMetadata parentMetadata, String metadataName, String prefix) throws AmetysRepositoryException, SAXException
215    {
216        BinaryMetadata value = parentMetadata.getBinaryMetadata(metadataName);
217        String filename = value.getFilename();
218        AttributesImpl attrs = new AttributesImpl();
219        attrs.addCDATAAttribute("type", "metadata");
220        attrs.addCDATAAttribute("mime-type", value.getMimeType());
221        attrs.addCDATAAttribute("path", prefix + metadataName);
222        
223        if (filename != null)
224        {
225            attrs.addCDATAAttribute("filename", filename);
226        }
227        attrs.addCDATAAttribute("size", String.valueOf(value.getLength()));
228        attrs.addCDATAAttribute("lastModified", ParameterHelper.valueToString(value.getLastModified()));
229        
230        XMLUtils.createElement(contentHandler, metadataName, attrs);
231    }
232    
233    /**
234     * SAX a rich-text metadata.
235     * @param contentHandler the content handler where to SAX into.
236     * @param parentMetadata the parent composite metadata.
237     * @param metadataName the metadata name.
238     * @param prefix the metadata path prefix.
239     * @throws AmetysRepositoryException if an error occurs.
240     * @throws SAXException if an error occurs.
241     * @throws IOException if an error occurs.
242     */
243    protected void _saxRichTextMetadata(ContentHandler contentHandler, CompositeMetadata parentMetadata, String metadataName, String prefix) throws AmetysRepositoryException, SAXException, IOException
244    {
245        RichText richText = parentMetadata.getRichText(metadataName);
246        AttributesImpl attrs = new AttributesImpl();
247        
248        attrs.addCDATAAttribute("mime-type", richText.getMimeType());
249        attrs.addCDATAAttribute("lastModified", ParameterHelper.valueToString(richText.getLastModified()));
250        
251        XMLUtils.startElement(contentHandler, metadataName, attrs);
252        
253        String encoding = richText.getEncoding();
254        
255        try (InputStream is = richText.getInputStream())
256        {
257            XMLUtils.data(contentHandler, IOUtils.toString(is, encoding));
258        }
259        
260        XMLUtils.endElement(contentHandler, metadataName);
261    }
262    
263    /**
264     * SAX a string metadata.
265     * @param contentHandler the content handler where to SAX into.
266     * @param parentMetadata the parent composite metadata.
267     * @param metadataName the metadata name.
268     * @param prefix the metadata path prefix.
269     * @throws AmetysRepositoryException if an error occurs.
270     * @throws SAXException if an error occurs.
271     */
272    protected void _saxStringMetadata(ContentHandler contentHandler, CompositeMetadata parentMetadata, String metadataName, String prefix) throws AmetysRepositoryException, SAXException
273    {
274        String[] values = parentMetadata.getStringArray(metadataName, new String[0]);
275        
276        for (String value : values)
277        {
278            XMLUtils.createElement(contentHandler, metadataName, value);
279        }
280    }
281    
282    /**
283     * SAX a multilingual string metadata.
284     * @param contentHandler the content handler where to SAX into.
285     * @param parentMetadata the parent composite metadata.
286     * @param metadataName the metadata name.
287     * @param prefix the metadata path prefix
288     * @param currentLocale The current locale. Can be null.
289     * @throws AmetysRepositoryException if an error occurs.
290     * @throws SAXException if an error occurs.
291     */
292    protected void _saxMultilingualStringMetadata(ContentHandler contentHandler, CompositeMetadata parentMetadata, String metadataName, String prefix, Locale currentLocale) throws AmetysRepositoryException, SAXException
293    {
294        if (parentMetadata.hasMetadata(metadataName))
295        {
296            MultilingualString multilingualString = parentMetadata.getMultilingualString(metadataName);
297            
298            if (currentLocale == null)
299            {
300                XMLUtils.startElement(contentHandler, metadataName);
301                for (Locale locale : multilingualString.getLocales())
302                {
303                    XMLUtils.createElement(contentHandler, locale.toString(), multilingualString.getValue(locale));
304                }
305                XMLUtils.endElement(contentHandler, metadataName);
306            }
307            else if (multilingualString.hasLocale(currentLocale))
308            {
309                AttributesImpl attr = new AttributesImpl();
310                attr.addCDATAAttribute("lang", currentLocale.toString());
311                XMLUtils.createElement(contentHandler, metadataName, attr, multilingualString.getValue(currentLocale));
312            }
313            else
314            {
315                // Get list of locales to search through, switching to default en if not found
316                List<Locale> localeLookupList = LocaleUtils.localeLookupList(currentLocale, new Locale("en"));
317                for (Locale locale : localeLookupList)
318                {
319                    if (multilingualString.hasLocale(locale))
320                    {
321                        AttributesImpl attr = new AttributesImpl();
322                        attr.addCDATAAttribute("lang", locale.toString());
323                        XMLUtils.createElement(contentHandler, metadataName, attr, multilingualString.getValue(locale));
324                        return;
325                    }
326                }
327                
328                // No locale found, get the first stored locale
329                Set<Locale> allLocales = multilingualString.getLocales();
330                if (!allLocales.isEmpty())
331                {
332                    Locale firstLocale = allLocales.iterator().next();
333                    AttributesImpl attr = new AttributesImpl();
334                    attr.addCDATAAttribute("lang", firstLocale.toString());
335                    XMLUtils.createElement(contentHandler, metadataName, attr, multilingualString.getValue(firstLocale));
336                }
337            }
338        }
339    }
340    
341    /**
342     * SAX a string metadata.
343     * @param contentHandler the content handler where to SAX into.
344     * @param parentMetadata the parent composite metadata.
345     * @param metadataName the metadata name.
346     * @param enumerator The enumerator for labelisable values
347     * @throws AmetysRepositoryException if an error occurs.
348     * @throws SAXException if an error occurs.
349     */
350    protected void _saxEnumeratedStringMetadata(ContentHandler contentHandler, CompositeMetadata parentMetadata, String metadataName, Enumerator enumerator) throws AmetysRepositoryException, SAXException
351    {
352        String[] values = parentMetadata.getStringArray(metadataName, new String[0]);
353        
354        for (String value : values)
355        {
356            if (StringUtils.isEmpty(value))
357            {
358                XMLUtils.createElement(contentHandler, metadataName);
359            }
360            else
361            {
362                AttributesImpl attrs = new AttributesImpl();
363                attrs.addCDATAAttribute("value", value);
364                
365                try
366                {
367                    I18nizableText i18n = enumerator.getEntry(value);
368                    if (i18n != null)
369                    {
370                        XMLUtils.startElement(contentHandler, metadataName, attrs);
371                        i18n.toSAX(contentHandler);
372                        XMLUtils.endElement(contentHandler, metadataName);
373                    }
374                    else
375                    {
376                        XMLUtils.createElement(contentHandler, metadataName, attrs, value);
377                    }
378                }
379                catch (Exception e)
380                {
381                    getLogger().warn("Saxing enumerated String metadata '" + metadataName + "' required a label for enumerated value", e);
382                    XMLUtils.createElement(contentHandler, metadataName, attrs, value);
383                }
384            }
385            
386        }
387    }
388    
389    /**
390     * SAX a metadata as a Date.
391     * @param contentHandler the content handler where to SAX into.
392     * @param parentMetadata the parent composite metadata.
393     * @param metadataName the metadata name.
394     * @param prefix the metadata path prefix.
395     * @throws AmetysRepositoryException if an error occurs.
396     * @throws SAXException if an error occurs.
397     */
398    protected void _saxDateMetadata(ContentHandler contentHandler, CompositeMetadata parentMetadata, String metadataName, String prefix) throws AmetysRepositoryException, SAXException
399    {
400        Date[] values = parentMetadata.getDateArray(metadataName, new Date[0]);
401        
402        for (Date value : values)
403        {
404            XMLUtils.createElement(contentHandler, metadataName, ParameterHelper.valueToString(value));
405        }
406    }
407    
408    /**
409     * SAX a metadata as a User.
410     * @param contentHandler the content handler where to SAX into.
411     * @param parentMetadata the parent composite metadata.
412     * @param metadataName the metadata name.
413     * @throws SAXException if an error occurs
414     */
415    protected void _saxUserMetadata(ContentHandler contentHandler, CompositeMetadata parentMetadata, String metadataName) throws SAXException
416    {
417        UserIdentity[] values = parentMetadata.getUserArray(metadataName);
418        
419        for (UserIdentity userIdentity : values)
420        {
421            User user = _userManager.getUser(userIdentity);
422            if (user != null)
423            {
424                AttributesImpl attrs = new AttributesImpl();
425                attrs.addCDATAAttribute("login", userIdentity.getLogin());
426                attrs.addCDATAAttribute("populationId", userIdentity.getPopulationId());
427                attrs.addCDATAAttribute("email", user.getEmail());
428                XMLUtils.createElement(contentHandler, metadataName, attrs, user.getFullName());
429            }
430        }
431    }
432
433    /**
434     * SAX a metadata as a User as JSON.
435     * @param contentHandler the content handler where to SAX into.
436     * @param parentMetadata the parent composite metadata.
437     * @param metadataName the metadata name.
438     * @throws SAXException if an error occurs
439     */
440    protected void _saxSingleUserMetadataAsJson(ContentHandler contentHandler, CompositeMetadata parentMetadata, String metadataName) throws SAXException
441    {
442        UserIdentity userIdentity = parentMetadata.getUser(metadataName);
443        
444        Map<String, Object> values = _userAsJson(userIdentity);
445        AttributesImpl attrs = new AttributesImpl();
446        attrs.addCDATAAttribute("json", "true");
447        
448        String jsonString = _jsonUtils.convertObjectToJson(values);
449        XMLUtils.createElement(contentHandler, metadataName, attrs, jsonString);
450    }
451    
452    /**
453     * SAX a metadata as a User as JSON.
454     * @param contentHandler the content handler where to SAX into.
455     * @param parentMetadata the parent composite metadata.
456     * @param metadataName the metadata name.
457     * @throws SAXException if an error occurs
458     */
459    protected void _saxMultipleUserMetadataAsJson(ContentHandler contentHandler, CompositeMetadata parentMetadata, String metadataName) throws SAXException
460    {
461        UserIdentity[] userIdentities = parentMetadata.getUserArray(metadataName);
462        
463        List<Map<String, Object>> values = new ArrayList<>();
464        for (UserIdentity userIdentity : userIdentities)
465        {
466            values.add(_userAsJson(userIdentity));
467        }
468        
469        AttributesImpl attrs = new AttributesImpl();
470        attrs.addCDATAAttribute("json", "true");
471        
472        String jsonString = _jsonUtils.convertObjectToJson(values);
473        XMLUtils.createElement(contentHandler, metadataName, attrs, jsonString);
474    }
475    
476    /**
477     * SAX an Object, i.e. its ID and name, and optionally its metadata. 
478     * @param contentHandler the content handler where to SAX into.
479     * @param object the {@link AmetysObject} to sax.
480     * @param prefix the metadata path prefix.
481     * @param isDeep If <code>true</code> and the AmetysObject is a {@link MetadataAwareAmetysObject}, its metadata will be saxed.
482     * @param locale The locale to use for localized metadata, such as {@link MultilingualString}. Can be null to sax all existing locales.
483     * @throws AmetysRepositoryException if an error occurs.
484     * @throws SAXException if an error occurs.
485     * @throws IOException if an error occurs.
486     */
487    protected void _saxObject(ContentHandler contentHandler, AmetysObject object, String prefix, boolean isDeep, Locale locale)  throws AmetysRepositoryException, IOException, SAXException
488    {
489        AttributesImpl attrs = new AttributesImpl();
490        attrs.addCDATAAttribute("id", object.getId());
491        attrs.addCDATAAttribute("name", object.getName());
492        
493        XMLUtils.startElement(contentHandler, "object", attrs);
494        
495        if (object instanceof MetadataAwareAmetysObject && isDeep)
496        {
497            CompositeMetadata metaHolder = ((MetadataAwareAmetysObject) object).getMetadataHolder();
498            
499            _saxAllMetadata(contentHandler, metaHolder, prefix, locale);
500        }
501        
502        XMLUtils.endElement(contentHandler, "object");
503    }
504    
505    /**
506     * Get the id of child Ametys objects
507     * @param objectCollection The parent object collection
508     * @return The id of child Ametys objects
509     */
510    protected List<String> _getRefAmetysObjectIds (TraversableAmetysObject objectCollection)
511    {
512        List<String> refAOs = new ArrayList<>();
513        
514        try (AmetysObjectIterable<AmetysObject> children = objectCollection.getChildren())
515        {
516            for (AmetysObject refAO : children)
517            {
518                refAOs.add(refAO.getId());
519            }
520        }
521        
522        return refAOs;
523    }
524    
525    /**
526     * Get the JSON representation of a User metadata
527     * @param userIdentity The user identity
528     * @return The user as JSON
529     */
530    protected Map<String, Object> _userAsJson (UserIdentity userIdentity)
531    {
532        Map<String, Object> json = new LinkedHashMap<>();
533        
534        json.put("login", userIdentity.getLogin());
535        json.put("populationId", userIdentity.getPopulationId());
536        
537        return json;
538    }
539    
540    /**
541     * Get the JSON representation of a {@link BinaryMetadata}
542     * @param binaryMetadata The metadata
543     * @param prefix The prefix
544     * @param metadataName The metadata name
545     * @return The binary as JSON
546     */
547    protected Map<String, Object> _binaryAsJson (BinaryMetadata binaryMetadata, String prefix, String metadataName)
548    {
549        Map<String, Object> json = new LinkedHashMap<>();
550        
551        String filename = binaryMetadata.getFilename();
552            
553        json.put("type", "metadata");
554        json.put("mimeType", binaryMetadata.getMimeType());
555        json.put("path", prefix + metadataName);
556            
557        if (filename != null)
558        {
559            json.put("filename", filename);
560        }
561        json.put("size", String.valueOf(binaryMetadata.getLength()));
562        json.put("lastModified", ParameterHelper.valueToString(binaryMetadata.getLastModified()));
563            
564        return json;
565    }
566}