001/*
002 *  Copyright 2013 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.transformation.htmledition;
018
019import java.util.ArrayList;
020import java.util.Iterator;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.cocoon.components.ContextHelper;
025import org.apache.cocoon.environment.ObjectModelHelper;
026import org.xml.sax.Attributes;
027import org.xml.sax.SAXException;
028
029import org.ametys.cms.data.RichText;
030import org.ametys.plugins.repository.metadata.ModifiableRichText;
031
032/**
033 * This transformer extracts semantic annotation from the incoming HTML for further processing.
034 * Appends all text nodes inside <span annotation="xxx"< element.
035 */
036public class SemanticAnnotationsEditionHandler extends AbstractHTMLEditionHandler
037{
038    private static final String __ANNOTATION_TAG_NAME = "span";
039    private static final String __ANNOTATION_ATTRIBUTE = "annotation";
040    
041    SemanticAnnotationsHolder _annotationsHolder;
042    
043    /**
044     * When document starts, a holder of semanticAnnotations is created
045     * @see org.ametys.cms.transformation.htmledition.AbstractHTMLEditionHandler#startDocument()
046     */
047    @Override
048    public void startDocument() throws SAXException
049    {
050        Map objectModel = ContextHelper.getObjectModel(_context);
051        Map parentContextParameters = (Map) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
052        Object richTextObject = parentContextParameters.get("richText");
053        
054        if (richTextObject != null)
055        {
056            if (richTextObject instanceof RichText)
057            {
058                RichText richText = (RichText) richTextObject;
059                
060                // Remove all existing annotations from the rich text node.
061                richText.removeAllAnnotations();
062                // Create a holder to write the annotations to.
063                _annotationsHolder = new SemanticAnnotationsHolder(richText);
064            }
065            else if (richTextObject instanceof ModifiableRichText)
066            {
067                ModifiableRichText richText = (ModifiableRichText) richTextObject;
068                
069                // Remove all existing annotations from the rich text node.
070                richText.removeAnnotations();
071                // Create a holder to write the annotations to.
072                _annotationsHolder = new SemanticAnnotationsHolderOld(richText);
073            }
074        }
075        
076        super.startDocument();
077    }
078        
079    /**
080     * Dispatch the startElement event to semanticAnnotations. Add a new annotation in the holder if the element matches a semantic annotation element
081     * @see org.ametys.cms.transformation.htmledition.AbstractHTMLEditionHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
082     */
083    @Override
084    public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException
085    {                
086        if (_annotationsHolder != null)
087        {
088            // Existing semantic annotations continue being saxed
089            _annotationsHolder.startElement();
090            
091            // A new semantic annotation starts being saxed
092            if (__ANNOTATION_TAG_NAME.equals(loc) && attrs.getValue(__ANNOTATION_ATTRIBUTE) != null) 
093            {
094                // We will now track its annotationValue through the SAX events
095                _annotationsHolder.newSaxedSemanticAnnotation(attrs.getValue(__ANNOTATION_ATTRIBUTE));
096            }
097        }
098        super.startElement(uri, loc, raw, attrs);
099    }
100        
101    /**
102     * Dispatch the characters event to semanticAnnotations.
103     * @see org.ametys.cms.transformation.htmledition.AbstractHTMLEditionHandler#characters(char[], int, int)
104     */
105    @Override
106    public void characters(char[] ch, int start, int length) throws SAXException
107    {
108        if (_annotationsHolder != null)
109        {
110            _annotationsHolder.characters(ch, start, length);
111        }
112        
113        super.characters(ch, start, length);
114    }
115    
116    /**
117     * Dispatch the endElement event to semanticAnnotations.
118     * @see org.ametys.cms.transformation.htmledition.AbstractHTMLEditionHandler#characters(char[], int, int)
119     */
120    @Override
121    public void endElement(String uri, String loc, String raw) throws SAXException
122    {    
123        if (_annotationsHolder != null)
124        {
125            _annotationsHolder.endElement();
126        }
127                
128        super.endElement(uri, loc, raw);
129    }
130    
131    /**
132     * A placeholder for saxedSemanticAnnotations
133     * Maintains a list of the saxedSemanticAnnotations.
134     * Dispatch SAX events to each saxedSemanticAnnotation.
135     */
136    private class SemanticAnnotationsHolder
137    {
138        protected List<AbstractSaxedSemanticAnnotation> _saxedSemanticAnnotations = new ArrayList<>();
139        RichText _richText;
140        
141        public SemanticAnnotationsHolder(RichText richText)
142        {
143            _richText = richText;
144        }
145        
146        public void startElement() 
147        {
148            for (AbstractSaxedSemanticAnnotation saxedSemanticAnnotation : _saxedSemanticAnnotations)
149            {
150                saxedSemanticAnnotation.startElement();
151            }
152        }
153        
154        public void endElement() throws SAXException
155        {
156            for (Iterator<AbstractSaxedSemanticAnnotation> it = _saxedSemanticAnnotations.iterator(); it.hasNext();)
157            {
158                AbstractSaxedSemanticAnnotation saxedSemanticAnnotation = it.next();
159                try
160                {
161                    saxedSemanticAnnotation.endElement();
162                    // Delete (finished) saxedSemanticAnnotation
163                    if (saxedSemanticAnnotation.hasFinished())
164                    {
165                        it.remove();
166                    }
167                }
168                catch (Exception e)
169                {
170                    String annotationName = saxedSemanticAnnotation.getCurrentAnnotationName();
171                    throw new SAXException("Unable to save semantic annotation " + annotationName + " in repository", e);
172                }
173            }
174        }
175        
176        public void characters(char[] ch, int start, int length)
177        {
178            for (AbstractSaxedSemanticAnnotation saxedSemanticAnnotation : _saxedSemanticAnnotations)
179            {
180                saxedSemanticAnnotation.characters(ch, start, length);
181            }
182        }
183        
184        /**
185         * Add a new semantic annotation in the holder 
186         * @param annotationName The name of the semantic annotation
187         */
188        public void newSaxedSemanticAnnotation(String annotationName)
189        {
190            if (_richText != null)
191            {
192                AbstractSaxedSemanticAnnotation saxedSemanticAnnotation = new AbstractSaxedSemanticAnnotation(annotationName)
193                {
194                    /**
195                     * A SaxedSemanticAnnotation  persists itself to JCR on its endElement.
196                     * @see org.ametys.cms.transformation.htmledition.SemanticAnnotationsEditionHandler.AbstractSaxedSemanticAnnotation#onAnnotationReady()
197                     */
198                    @Override
199                    public void onAnnotationReady() throws Exception
200                    {
201                        // When the semAnnotation is fully saxed, persist it to JCR
202                        _richText.addAnnotations(getCurrentAnnotationName(), getCurrentAnnotationValue().toString());
203                    }
204                };
205                
206                _saxedSemanticAnnotations.add(saxedSemanticAnnotation);
207            }
208        }
209    }
210    
211    /**
212     * A semantic annotation defined through a serie of SAX events.
213     * The abstract onAnnotationReady() method takes care of the annotation as soon as it is fully SAXed.  
214     */
215    private abstract class AbstractSaxedSemanticAnnotation
216    {
217        private final String _currentAnnotationName;
218        private int _cptrElementsInsideSemanticAnnotation;
219        private final StringBuilder _currentAnnotationValue;
220        private boolean _hasFinished;
221        
222        public AbstractSaxedSemanticAnnotation(String annotationName)
223        {
224            this._currentAnnotationName = annotationName;
225            this._cptrElementsInsideSemanticAnnotation = 0;
226            this._currentAnnotationValue = new StringBuilder();
227            this._hasFinished = false;
228        }
229        
230        public void startElement() 
231        {
232            _cptrElementsInsideSemanticAnnotation++;
233        }
234        
235        public void endElement() throws Exception
236        {
237            if (_cptrElementsInsideSemanticAnnotation == 0)
238            {                
239                onAnnotationReady();
240                _hasFinished = true;
241            }
242            else 
243            {
244                _cptrElementsInsideSemanticAnnotation--;                
245            }            
246        }
247        
248        public abstract void onAnnotationReady() throws Exception;
249
250        public void characters(char[] ch, int start, int length)
251        {
252            _currentAnnotationValue.append(ch, start, length);
253        }
254
255        public String getCurrentAnnotationName()
256        {
257            return _currentAnnotationName;
258        }
259
260
261        public StringBuilder getCurrentAnnotationValue()
262        {
263            return _currentAnnotationValue;
264        }
265
266
267        public boolean hasFinished()
268        {
269            return _hasFinished;
270        }
271    }
272    /**
273     * A placeholder for saxedSemanticAnnotations
274     * Maintains a list of the saxedSemanticAnnotations.
275     * Dispatch SAX events to each saxedSemanticAnnotation.
276     */
277    @Deprecated
278    private class SemanticAnnotationsHolderOld extends SemanticAnnotationsHolder
279    {
280        @SuppressWarnings("hiding")
281        final ModifiableRichText _richText;
282        
283        public SemanticAnnotationsHolderOld(ModifiableRichText richText)
284        {
285            super(null);
286            this._richText = richText;
287        }
288        
289        @Override
290        public void newSaxedSemanticAnnotation(String annotationName)
291        {
292            if (_richText != null)
293            {
294                AbstractSaxedSemanticAnnotation saxedSemanticAnnotation = new AbstractSaxedSemanticAnnotation(annotationName)
295                {
296                    /**
297                     * A SaxedSemanticAnnotation  persists itself to JCR on its endElement.
298                     * @see org.ametys.cms.transformation.htmledition.SemanticAnnotationsEditionHandler.AbstractSaxedSemanticAnnotation#onAnnotationReady()
299                     */
300                    @Override
301                    public void onAnnotationReady() throws Exception
302                    {
303                        // When the semAnnotation is fully saxed, persist it to JCR
304                        _richText.addAnnotation(getCurrentAnnotationName(), getCurrentAnnotationValue().toString());
305                    }
306                };
307                _saxedSemanticAnnotations.add(saxedSemanticAnnotation);
308            }
309        }
310    }
311}