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.plugins.repository.metadata.ModifiableRichText;
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        ModifiableRichText richText = (ModifiableRichText) parentContextParameters.get("richText");
052        
053        if (richText != null)
054        {
055            // Remove all existing annotations from the rich text node.
056            richText.removeAnnotations();
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        final ModifiableRichText _richText;
124        private final List<AbstractSaxedSemanticAnnotation> _saxedSemanticAnnotations = new ArrayList<>();
125        
126        
127        public SemanticAnnotationsHolder(ModifiableRichText richText)
128        {
129            this._richText = richText;
130        }
131        
132        public void startElement() 
133        {
134            for (AbstractSaxedSemanticAnnotation saxedSemanticAnnotation : _saxedSemanticAnnotations)
135            {
136                saxedSemanticAnnotation.startElement();
137            }
138        }
139        
140        public void endElement() throws SAXException
141        {
142            for (Iterator<AbstractSaxedSemanticAnnotation> it = _saxedSemanticAnnotations.iterator(); it.hasNext();)
143            {
144                AbstractSaxedSemanticAnnotation saxedSemanticAnnotation = it.next();
145                try
146                {
147                    saxedSemanticAnnotation.endElement();
148                    // Delete (finished) saxedSemanticAnnotation
149                    if (saxedSemanticAnnotation.hasFinished())
150                    {
151                        it.remove();
152                    }
153                }
154                catch (Exception e)
155                {
156                    String annotationName = saxedSemanticAnnotation.getCurrentAnnotationName();
157                    throw new SAXException("Unable to save semantic annotation " + annotationName + " in repository", e);
158                }
159            }
160        }
161        
162        public void characters(char[] ch, int start, int length)
163        {
164            for (AbstractSaxedSemanticAnnotation saxedSemanticAnnotation : _saxedSemanticAnnotations)
165            {
166                saxedSemanticAnnotation.characters(ch, start, length);
167            }
168        }
169        
170        /**
171         * Add a new semantic annotation in the holder 
172         * @param annotationName The name of the semantic annotation
173         */
174        public void newSaxedSemanticAnnotation(String annotationName)
175        {
176            if (_richText != null)
177            {
178                AbstractSaxedSemanticAnnotation saxedSemanticAnnotation = new AbstractSaxedSemanticAnnotation(annotationName)
179                {
180                    /**
181                     * A SaxedSemanticAnnotation  persists itself to JCR on its endElement.
182                     * @see org.ametys.cms.transformation.htmledition.SemanticAnnotationsEditionHandler.AbstractSaxedSemanticAnnotation#onAnnotationReady()
183                     */
184                    @Override
185                    public void onAnnotationReady() throws Exception
186                    {
187                        // When the semAnnotation is fully saxed, persist it to JCR
188                        _richText.addAnnotation(getCurrentAnnotationName(), getCurrentAnnotationValue().toString());
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}