001/*
002 *  Copyright 2020 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 */
016
017package org.ametys.cms.data;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.net.URL;
022import java.util.Map;
023
024import org.apache.avalon.framework.component.Component;
025import org.apache.avalon.framework.context.ContextException;
026import org.apache.avalon.framework.context.Contextualizable;
027import org.apache.cocoon.Constants;
028import org.apache.cocoon.environment.Context;
029import org.apache.cocoon.xml.AttributesImpl;
030import org.apache.excalibur.xml.sax.ContentHandlerProxy;
031import org.xml.sax.Attributes;
032import org.xml.sax.ContentHandler;
033import org.xml.sax.SAXException;
034
035import org.ametys.cms.repository.comment.CommentsDAO;
036
037/**
038 * Factory for the transformer that imports a rich text from docbook.
039 */
040public class RichTextImportHandlerFactory implements Component, Contextualizable
041{
042    /** Avalon role. */
043    public static final String ROLE = RichTextImportHandlerFactory.class.getName();
044    private Context _cocoonContext;
045    
046    public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException
047    {
048        _cocoonContext = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
049    }
050
051    /**
052     * Creates a handler proxy to import the rich text
053     * @param contentHandler the contentHandler to pass SAX events to
054     * @param richText the rich text
055     * @param files the attachments of this rich text
056     * @return the created handler
057     */
058    public RichTextImportHandler createHandlerProxy(ContentHandler contentHandler, RichText richText, Map<String, InputStream> files)
059    {
060        return new RichTextImportHandler(contentHandler, richText, files);
061    }
062    
063    /**
064     * This transformer imports the rich text from docbook.
065     */
066    public class RichTextImportHandler extends ContentHandlerProxy
067    {
068        private static final String __ATTACHMENT_IMAGE_TAG_NAME = "imagedata";
069        private static final String __ATTACHMENT_VIDEO_TAG_NAME = "videodata";
070        private static final String __ATTACHMENT_AUDIO_TAG_NAME = "audiodata";
071        private static final String __ATTACHMENT_TYPE_ATTRIBUTE_NAME = "type";
072        private static final String __ATTACHMENT_TYPE_ATTRIBUTE_LOCAL_VALUE = "local";
073
074        private static final String __ANNOTATION_TAG_NAME = "phrase";
075        private static final String __ANNOTATION_NAME_ATTRIBUTE_NAME = "role";
076        private static final String __ANNOTATION_CLASS_ATTRIBUTE_NAME = "class";
077        private static final String __ANNOTATION_CLASS_ATTRIBUTE_VALUE = "semantic";
078
079
080        private RichText _richText;
081        private Map<String, InputStream> _files;
082
083        private boolean _isCurrentlyInAnnotation;
084        private String _currentAnnotationName;
085        private StringBuilder _currentAnnotationValue;
086        private int _cptrElementsInsideCurrentAnnotation;
087        
088        /**
089         * Creates a handler proxy to import a rich text
090         * @param contentHandler the contentHandler to pass SAX events to
091         * @param richText the rich text
092         * @param files the attachments of this rich text
093         */
094        public RichTextImportHandler(ContentHandler contentHandler, RichText richText, Map<String, InputStream> files)
095        {
096            super(contentHandler);
097            _richText = richText;
098            _files = files;
099        } 
100
101        @Override
102        public void startDocument() throws SAXException
103        {
104            // Remove all existing attachments from the rich text.
105            _richText.removeAttachments();
106
107            // Remove all existing annotations from the rich text.
108            _richText.removeAllAnnotations();
109
110            super.startDocument();
111        }
112
113        @Override
114        public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException
115        {
116            // A new attachment starts being saxed
117            boolean isAttachment = _isAttachment(loc);
118            String type = attrs.getValue(__ATTACHMENT_TYPE_ATTRIBUTE_NAME);
119            Attributes newAttrs = attrs;
120            if (isAttachment && __ATTACHMENT_TYPE_ATTRIBUTE_LOCAL_VALUE.equals(type))
121            {
122                newAttrs = _processAttachment(attrs);
123            }
124
125            // A new semantic annotation starts being saxed
126            String clazz = attrs.getValue(__ANNOTATION_CLASS_ATTRIBUTE_NAME);
127            String annotationName = attrs.getValue(__ANNOTATION_NAME_ATTRIBUTE_NAME);
128            if (__ANNOTATION_TAG_NAME.equals(loc) && __ANNOTATION_CLASS_ATTRIBUTE_VALUE.equals(clazz) && annotationName != null) 
129            {
130                _processAnnotation(attrs);
131            }
132            else if (_isCurrentlyInAnnotation)
133            {
134                // A new element is being SAXed inside the current annotation
135                _cptrElementsInsideCurrentAnnotation++;
136            }
137
138            super.startElement(uri, loc, raw, newAttrs);
139        }
140        
141        private boolean _isAttachment(String loc)
142        {
143            return __ATTACHMENT_IMAGE_TAG_NAME.equals(loc) || __ATTACHMENT_VIDEO_TAG_NAME.equals(loc) || __ATTACHMENT_AUDIO_TAG_NAME.equals(loc);
144        }
145
146        private Attributes _processAttachment(Attributes attrs) throws SAXException
147        {
148            String fileRefAttribute = attrs.getValue("fileref");
149            String filename;
150            
151            if (CommentsDAO.URL_VALIDATOR.matcher(fileRefAttribute).matches())
152            {
153                try
154                {
155                    NamedResource attachment = new NamedResource();
156    
157                    URL url = new URL(fileRefAttribute);
158                    try (InputStream is = url.openStream())
159                    {
160                        attachment.setInputStream(is);
161                    }
162                    String path = url.getPath();
163                    filename = path.substring(path.lastIndexOf("/") + 1);
164    
165                    String mimeType = _cocoonContext.getMimeType(filename.toLowerCase());
166                    attachment.setMimeType(mimeType);
167                    attachment.setFilename(filename);
168                    _richText.addAttachment(attachment);
169                }
170                catch (IOException e)
171                {
172                    throw new SAXException("Unable to process the attachment '" + fileRefAttribute + "'. An error occured while setting its content", e);
173                }
174            }
175            else
176            {
177                // file reference is of the form ownerId@dataName;fileName
178                int indexOfFilenameSeparator = fileRefAttribute.lastIndexOf(';');
179                filename = fileRefAttribute.substring(indexOfFilenameSeparator + 1);
180    
181                if (indexOfFilenameSeparator == -1)
182                {
183                    throw new IllegalArgumentException("A local image should have a file reference of the form <protocol>://<protocol-specific-part>;<filename> : " + fileRefAttribute);
184                }
185    
186                if (_files.containsKey(filename))
187                {
188                    try
189                    {
190                        NamedResource attachment = new NamedResource();
191                        String mimeType = _cocoonContext.getMimeType(filename.toLowerCase());
192                        attachment.setMimeType(mimeType);
193                        attachment.setFilename(filename);
194                        attachment.setInputStream(_files.get(filename));
195                        _richText.addAttachment(attachment);
196                    }
197                    catch (IOException e)
198                    {
199                        throw new SAXException("Unable to process the attachment '" + filename + "'. An error occured while setting its content", e);
200                    }
201                }
202            }
203            
204            AttributesImpl newAttrs = new AttributesImpl();
205            _copyAttributes(attrs, newAttrs);
206            newAttrs.addCDATAAttribute("fileref", filename);
207            return newAttrs;
208        }
209        
210        /**
211         * Copy the attributes except the fileref attribute
212         * @param attrs the attributes to copy.
213         * @param newAttrs the attributes to copy to.
214         */
215        private void _copyAttributes(Attributes attrs, AttributesImpl newAttrs)
216        {
217            for (int i = 0; i < attrs.getLength(); i++)
218            {
219                String name = attrs.getQName(i);
220
221                if (!"fileref".equals(name))
222                {
223                    newAttrs.addAttribute(attrs.getURI(i), attrs.getLocalName(i), name, attrs.getType(i), attrs.getValue(i));
224                }
225            }
226        }
227
228        private void _processAnnotation(Attributes attrs)
229        {
230            _isCurrentlyInAnnotation = true;
231            _currentAnnotationName = attrs.getValue(__ANNOTATION_NAME_ATTRIBUTE_NAME);
232            _currentAnnotationValue = new StringBuilder();
233            _cptrElementsInsideCurrentAnnotation = 0;
234        }
235
236        @Override
237        public void characters(char[] ch, int start, int length) throws SAXException
238        {
239            if (_isCurrentlyInAnnotation)
240            {
241                _currentAnnotationValue.append(ch, start, length);
242            }
243            
244            super.characters(ch, start, length);
245        }
246
247        @Override
248        public void endElement(String uri, String loc, String raw) throws SAXException
249        {    
250            if (_isCurrentlyInAnnotation)
251            {
252                if (_cptrElementsInsideCurrentAnnotation == 0)
253                {                
254                    // When the semantic annotation is fully saxed, add it to the rich text
255                    _richText.addAnnotations(_currentAnnotationName, _currentAnnotationValue.toString());
256                    _isCurrentlyInAnnotation = false;
257                }
258                else 
259                {
260                    _cptrElementsInsideCurrentAnnotation--;                
261                }
262            }
263            
264            super.endElement(uri, loc, raw);
265        }
266    }
267}