001/*
002 *  Copyright 2023 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.cms.transformation;
017
018import java.util.HashMap;
019import java.util.Map;
020
021import org.apache.cocoon.xml.AttributesImpl;
022import org.apache.cocoon.xml.XMLUtils;
023import org.xml.sax.Attributes;
024import org.xml.sax.ContentHandler;
025import org.xml.sax.Locator;
026import org.xml.sax.SAXException;
027
028import org.ametys.core.util.I18nUtils;
029import org.ametys.runtime.config.Config;
030import org.ametys.runtime.i18n.I18nizableText;
031import org.ametys.runtime.model.Model;
032import org.ametys.runtime.model.ModelItem;
033import org.ametys.runtime.model.type.DataContext;
034import org.ametys.runtime.model.type.ElementType;
035
036/**
037 * ContentHandler able to interpret configuration and transform it to XML value.
038 * 
039 * For example it will transform an element with an attribute type="i18n" to the translated message.
040 * 
041 * Currently interpretation are made for i18n and config.
042 * 
043 * In the case of i18n, the translated message will be processed by a parser, allowing to provide rich text value through configuration.
044 */
045public class Configuration2XMLValuesTransformer implements ContentHandler
046{
047    private DataContext _context;
048    private I18nUtils _i18nUtils;
049    private String _configType;
050    private ContentHandler _contentHandler;
051    
052    private StringBuilder _charBuffer;
053    private Map<String, Object> _configTypeParams;
054    private boolean _transformToDocbook;
055
056    /**
057     * Construct a content handler able to interpret configuration element.
058     * 
059     * For example, the text content of an element with attribute type="i18n" will be replace by the corresponding translated message
060     * 
061     * @param contentHandler sax event will be forwarded to this content handler after possible interpretation
062     * @param context the data context to use when interpreting the input
063     * @param i18nUtils the I18nUtils component. Use for translation
064     */
065    public Configuration2XMLValuesTransformer(ContentHandler contentHandler, DataContext context, I18nUtils i18nUtils)
066    {
067        _contentHandler = contentHandler;
068        _context = context;
069        _i18nUtils = i18nUtils;
070    }
071
072    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException
073    {
074        if ("true".equals(atts.getValue("transform-to-docbook")))
075        {
076            _transformToDocbook = true;
077            
078            _charBuffer = new StringBuilder();
079        }
080        
081        // local reference in case we need to edit the attributes
082        Attributes attrs = atts;
083        if (_is18n(atts))
084        {
085            _configType = "i18n";
086            _configTypeParams = new HashMap<>();
087            
088            _configTypeParams.put("catalogue", atts.getValue("catalogue"));
089            
090            _charBuffer = new StringBuilder();
091            
092            // clean attributes
093            AttributesImpl attrsCopy = new AttributesImpl(atts);
094            attrsCopy.removeAttribute("i18n");
095            attrsCopy.removeAttribute("type");
096            attrsCopy.removeAttribute("catalogue");
097            
098            attrs = attrsCopy;
099            _contentHandler.startElement(uri, localName, qName, attrs);
100        }
101        else if (_isConfig(atts))
102        {
103            _configType = "config";
104            
105            _charBuffer = new StringBuilder();
106            
107            // clean attributes
108            AttributesImpl attrsCopy = new AttributesImpl(atts);
109            attrsCopy.removeAttribute("type");
110            
111            attrs = attrsCopy;
112            // Do not start element, element will be created in the end element
113        }
114        else
115        {
116            _contentHandler.startElement(uri, localName, qName, attrs);
117        }
118    }
119
120    private boolean _is18n(Attributes atts)
121    {
122        return "true".equals(atts.getValue("i18n")) || "i18n".equals(atts.getValue("type"));
123    }
124
125    private boolean _isConfig(Attributes atts)
126    {
127        return "config".equals(atts.getValue("type"));
128    }
129    
130    public void characters(char[] ch, int start, int length) throws SAXException
131    {
132        if (_configType != null || _transformToDocbook)
133        {
134            for (int i = start; i < start + length; i++)
135            {
136                _charBuffer.append(ch[i]);
137            }
138        }
139        else
140        {
141            _contentHandler.characters(ch, start, length);
142        }
143    }
144
145    public void endElement(String uri, String localName, String qName) throws SAXException
146    {
147        if (_configType != null)
148        {
149            switch (_configType)
150            {
151                case "i18n":
152                    _configType = null;
153                    String translation = _i18nUtils.translate(new I18nizableText((String) _configTypeParams.get("catalogue"), _charBuffer.toString()), _context.getLocale().getLanguage());
154                    
155                    if (translation != null)
156                    {
157                        if (_transformToDocbook)
158                        {
159                            saxStringAsDockbook(translation);
160                        }
161                        else
162                        {
163                            _contentHandler.characters(translation.toCharArray(), 0, translation.length());
164                        }
165                    }
166
167                    _contentHandler.endElement(uri, localName, qName);
168                    break;
169                case "config":
170                    _configType = null;
171                    
172                    Model configModel = Config.getModel();
173                    if (configModel != null)
174                    {
175                        String configId = _charBuffer.toString();
176                        ModelItem configDefinition = configModel.getChild(configId);
177                        if (configDefinition != null && configDefinition instanceof ElementType elementType)
178                        {
179                            elementType.valueToSAX(_contentHandler, localName, Config.getInstance().getValue(configId), _context);
180                        }
181                        else
182                        {
183                            throw new SAXException("No config element is defined for id " + configId + ".");
184                        }
185                    }
186                    break;
187                default:
188                    throw new SAXException("Configuration type " + _configType + " is not supported.");
189            }
190        }
191        else
192        {
193            // in the case where we want to transform raw data
194            if (_transformToDocbook)
195            {
196                saxStringAsDockbook(_charBuffer.toString());
197            }
198            _contentHandler.endElement(uri, localName, qName);
199        }
200    }
201
202    public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException
203    {
204        if (_configType != null)
205        {
206            for (int i = start; i < start + length; i++)
207            {
208                _charBuffer.append(ch[i]);
209            }
210        }
211        else
212        {
213            if (_transformToDocbook)
214            {
215                saxStringAsDockbook(_charBuffer.toString());
216            }
217            _contentHandler.ignorableWhitespace(ch, start, length);
218        }
219    }
220    
221    private void saxStringAsDockbook(String string) throws SAXException
222    {
223        _transformToDocbook = false;
224        
225        _contentHandler.startPrefixMapping("", "http://docbook.org/ns/docbook");
226        _contentHandler.startPrefixMapping("xlink", "http://www.w3.org/1999/xlink");
227        _contentHandler.startPrefixMapping("html", "http://www.w3.org/1999/xhtml");
228        
229        AttributesImpl atts = new AttributesImpl();
230        atts.addCDATAAttribute("version", "5.0");
231        XMLUtils.startElement(_contentHandler, "article", atts);
232        XMLUtils.createElement(_contentHandler, "para", string);
233        XMLUtils.endElement(_contentHandler, "article");
234        
235        _contentHandler.endPrefixMapping("");
236        _contentHandler.endPrefixMapping("xlink");
237        _contentHandler.endPrefixMapping("html");
238    }
239
240    public void setDocumentLocator(Locator locator)
241    {
242        // ignore
243    }
244    
245    public void startDocument() throws SAXException
246    {
247        // ignore
248    }
249    
250    public void endDocument() throws SAXException
251    {
252        // ignore
253    }
254    
255    public void startPrefixMapping(String prefix, String uri) throws SAXException
256    {
257        // ignore
258    }
259    
260    public void endPrefixMapping(String prefix) throws SAXException
261    {
262        // ignore
263    }
264
265    public void processingInstruction(String target, String data) throws SAXException
266    {
267        // ignore
268    }
269
270    public void skippedEntity(String name) throws SAXException
271    {
272        // ignore
273    }
274
275}