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