001/* 002 * Copyright 2019 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.data.type; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.io.OutputStream; 021import java.nio.charset.StandardCharsets; 022import java.time.ZonedDateTime; 023import java.time.format.DateTimeFormatter; 024import java.util.LinkedHashMap; 025import java.util.Map; 026import java.util.Optional; 027import java.util.Properties; 028import java.util.stream.Stream; 029 030import javax.xml.transform.OutputKeys; 031import javax.xml.transform.Transformer; 032import javax.xml.transform.TransformerException; 033import javax.xml.transform.TransformerFactory; 034import javax.xml.transform.dom.DOMSource; 035import javax.xml.transform.stream.StreamResult; 036 037import org.apache.avalon.framework.service.ServiceException; 038import org.apache.avalon.framework.service.ServiceManager; 039import org.apache.cocoon.xml.AttributesImpl; 040import org.apache.cocoon.xml.XMLUtils; 041import org.apache.commons.io.IOUtils; 042import org.apache.commons.lang3.StringUtils; 043import org.apache.commons.lang3.tuple.Triple; 044import org.apache.excalibur.xml.sax.SAXParser; 045import org.w3c.dom.Element; 046import org.xml.sax.ContentHandler; 047import org.xml.sax.InputSource; 048import org.xml.sax.SAXException; 049 050import org.ametys.cms.data.RichText; 051import org.ametys.cms.data.RichTextImportMetadataHandlerFactory; 052import org.ametys.cms.data.RichTextImportMetadataHandlerFactory.RichTextImportMetadataHandler; 053import org.ametys.cms.transformation.RichTextTransformer; 054import org.ametys.cms.transformation.docbook.DocbookTransformer; 055import org.ametys.core.model.type.AbstractElementType; 056import org.ametys.core.model.type.ModelItemTypeHelper; 057import org.ametys.core.util.IgnoreRootHandler; 058import org.ametys.core.util.dom.DOMUtils; 059import org.ametys.plugins.repository.AmetysRepositoryException; 060import org.ametys.runtime.model.ViewItem; 061import org.ametys.runtime.model.compare.DataChangeType; 062import org.ametys.runtime.model.compare.DataChangeTypeDetail; 063import org.ametys.runtime.model.type.DataContext; 064 065/** 066 * Abstract class for rich text type of elements 067 */ 068public abstract class AbstractRichTextElementType extends AbstractElementType<RichText> 069{ 070 /** Rich text transformer */ 071 protected RichTextTransformer _richTextTransformer; 072 /** Rich text metadata handler */ 073 protected RichTextImportMetadataHandlerFactory _richTextHandlerFactory; 074 /** Avalon service manager */ 075 protected ServiceManager _manager; 076 077 @Override 078 public void service(ServiceManager manager) throws ServiceException 079 { 080 super.service(manager); 081 _richTextTransformer = (RichTextTransformer) manager.lookup(DocbookTransformer.ROLE); 082 _richTextHandlerFactory = (RichTextImportMetadataHandlerFactory) manager.lookup(RichTextImportMetadataHandlerFactory.ROLE); 083 _manager = manager; 084 } 085 086 @Override 087 public RichText convertValue(Object value) 088 { 089 if (value instanceof String) 090 { 091 return _fromString((String) value); 092 } 093 094 return super.convertValue(value); 095 } 096 097 @Override 098 public String toString(RichText value) 099 { 100 throw new UnsupportedOperationException("Unable to convert a rich text to a string value"); 101 } 102 103 public Object fromJSONForClient(Object json) 104 { 105 String content = (String) json; 106 107 if (StringUtils.isEmpty(content)) 108 { 109 return null; 110 } 111 112 return _fromString(content); 113 } 114 115 private RichText _fromString(String content) 116 { 117 RichText richText = new RichText(); 118 119 try 120 { 121 _richTextTransformer.transform(content, richText); 122 } 123 catch (AmetysRepositoryException | IOException e) 124 { 125 if (getLogger().isErrorEnabled()) 126 { 127 getLogger().error("Unable to transform rich text", e); 128 } 129 } 130 131 return richText; 132 } 133 134 @Override 135 protected Object _singleValueToJSON(RichText value, DataContext context) 136 { 137 Map<String, Object> richTextInfos = new LinkedHashMap<>(); 138 139 Optional.ofNullable(value.getMimeType()).ifPresent(mimeType -> richTextInfos.put("mime-type", mimeType)); 140 Optional.ofNullable(value.getLastModificationDate()).ifPresent(lastModificationDate -> richTextInfos.put("lastModified", lastModificationDate.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))); 141 142 try (InputStream is = value.getInputStream()) 143 { 144 String encoding = value.getEncoding(); 145 richTextInfos.put("value", IOUtils.toString(is, encoding)); 146 } 147 catch (IOException e) 148 { 149 throw new RuntimeException("Unable to sax the rich text value", e); 150 } 151 152 return richTextInfos; 153 } 154 155 @Override 156 protected RichText _singleValueFromXML(Element element, Optional<Object> additionalData) throws TransformerException, IOException 157 { 158 if (element != null) 159 { 160 RichText richText = new RichText(); 161 162 String mimeType = element.getAttribute("mime-type"); 163 String encoding = StringUtils.defaultIfBlank(element.getAttribute("encoding"), StandardCharsets.UTF_8.name()); 164 165 richText.setMimeType(mimeType); 166 richText.setEncoding(encoding); 167 richText.setLastModificationDate(ZonedDateTime.now()); 168 169 if (mimeType.equals("text/xml") || mimeType.equals("application/xml")) 170 { 171 try (OutputStream os = richText.getOutputStream()) 172 { 173 Element childElement = DOMUtils.getFirstChildElement(element); 174 if (childElement != null) 175 { 176 Transformer transformer = TransformerFactory.newInstance().newTransformer(); 177 178 Properties format = new Properties(); 179 format.put(OutputKeys.METHOD, "xml"); 180 format.put(OutputKeys.ENCODING, encoding); 181 format.put(OutputKeys.INDENT, "no"); 182 183 transformer.setOutputProperties(format); 184 185 DOMSource domSource = new DOMSource(childElement); 186 StreamResult result = new StreamResult(os); 187 188 transformer.transform(domSource, result); 189 } 190 } 191 } 192 else 193 { 194 String data = element.getTextContent(); 195 try (OutputStream os = richText.getOutputStream()) 196 { 197 os.write(data.getBytes(encoding)); 198 } 199 } 200 201 _parseAdditionalData(richText, additionalData); 202 203 return richText; 204 } 205 else 206 { 207 return null; 208 } 209 } 210 211 private void _parseAdditionalData(RichText richText, Optional<Object> additionalData) throws IOException 212 { 213 try 214 { 215 @SuppressWarnings("unchecked") 216 Map<String, InputStream> files = additionalData.isPresent() ? (Map<String, InputStream>) additionalData.get() : Map.of(); 217 RichTextImportMetadataHandler handler = _richTextHandlerFactory.createHandler(richText, files); 218 219 SAXParser saxParser = null; 220 try 221 { 222 saxParser = (SAXParser) _manager.lookup(SAXParser.ROLE); 223 saxParser.parse(new InputSource(richText.getInputStream()), handler); 224 } 225 catch (ServiceException e) 226 { 227 throw new RuntimeException("Unable to get a SAX parser", e); 228 } 229 finally 230 { 231 _manager.release(saxParser); 232 } 233 } 234 catch (SAXException e) 235 { 236 getLogger().warn("Unable to get the attachments and semantic annotations from the rich text.", e); 237 } 238 } 239 240 @Override 241 protected void _valueToSAXForEdition(ContentHandler contentHandler, String tagName, Object value, Optional<ViewItem> viewItem, DataContext context, AttributesImpl attributes) throws SAXException 242 { 243 if (value instanceof RichText) 244 { 245 _singleValueToSAXForEdition(contentHandler, tagName, (RichText) value, attributes); 246 } 247 else if (value instanceof RichText[]) 248 { 249 for (RichText richText : (RichText[]) value) 250 { 251 _singleValueToSAXForEdition(contentHandler, tagName, richText, attributes); 252 } 253 } 254 } 255 256 private void _singleValueToSAXForEdition(ContentHandler contentHandler, String tagName, RichText richText, AttributesImpl attributes) throws SAXException 257 { 258 AttributesImpl localAttributes = new AttributesImpl(attributes); 259 _addAttributesToSAX(richText, localAttributes); 260 XMLUtils.startElement(contentHandler, tagName, localAttributes); 261 262 StringBuilder result = new StringBuilder(2048); 263 264 try 265 { 266 _richTextTransformer.transformForEditing(richText, result); 267 } 268 catch (IOException e) 269 { 270 throw new SAXException("Unable to transform a rich text into a string", e); 271 } 272 273 XMLUtils.data(contentHandler, result.toString()); 274 275 XMLUtils.endElement(contentHandler, tagName); 276 } 277 278 @Override 279 protected void _singleValueToSAX(ContentHandler contentHandler, String tagName, RichText richText, Optional<ViewItem> viewItem, DataContext context, AttributesImpl attributes) throws SAXException, IOException 280 { 281 AttributesImpl localAttributes = new AttributesImpl(attributes); 282 _addAttributesToSAX(richText, localAttributes); 283 XMLUtils.startElement(contentHandler, tagName, localAttributes); 284 285 String mimeType = richText.getMimeType(); 286 if (mimeType.equals("text/xml") || mimeType.equals("application/xml")) 287 { 288 SAXParser saxParser = null; 289 try (InputStream is = richText.getInputStream()) 290 { 291 saxParser = (SAXParser) _manager.lookup(SAXParser.ROLE); 292 saxParser.parse(new InputSource(is), new IgnoreRootHandler(contentHandler)); 293 } 294 catch (ServiceException e) 295 { 296 throw new RuntimeException("Unable to get a SAX parser", e); 297 } 298 finally 299 { 300 _manager.release(saxParser); 301 } 302 } 303 else if (mimeType.equals("text/plain")) 304 { 305 try (InputStream is = richText.getInputStream()) 306 { 307 XMLUtils.data(contentHandler, IOUtils.toString(is, richText.getEncoding())); 308 } 309 } 310 else 311 { 312 throw new SAXException("Mime-type " + mimeType + " is not supported for the sax rich text"); 313 } 314 315 XMLUtils.endElement(contentHandler, tagName); 316 } 317 318 private void _addAttributesToSAX(RichText richText, AttributesImpl attributes) 319 { 320 String mimeType = richText.getMimeType(); 321 attributes.addCDATAAttribute("mime-type", mimeType); 322 Optional.ofNullable(richText.getLastModificationDate()).ifPresent(lastModificationDate -> attributes.addCDATAAttribute("lastModified", lastModificationDate.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))); 323 324 String encoding = _getNonNullEncoding(richText); 325 attributes.addCDATAAttribute("encoding", encoding); 326 } 327 328 private String _getNonNullEncoding(RichText richText) 329 { 330 String encoding = richText.getEncoding(); 331 if (encoding == null) 332 { 333 encoding = StandardCharsets.UTF_8.name(); 334 } 335 return encoding; 336 } 337 338 @Override 339 protected Stream<Triple<DataChangeType, DataChangeTypeDetail, String>> _compareMultipleValues(RichText[] value1, RichText[] value2) throws IOException 340 { 341 throw new UnsupportedOperationException("Unable to compare multiple values of type '" + getId() + "'"); 342 } 343 344 @Override 345 protected Stream<Triple<DataChangeType, DataChangeTypeDetail, String>> _compareSingleValues(RichText value1, RichText value2) throws IOException 346 { 347 if (ModelItemTypeHelper.areSingleObjectsBothNotNullAndDifferents(value1, value2)) 348 { 349 return ResourceElementTypeHelper.compareSingleRichTexts(value1, value2); 350 } 351 else 352 { 353 return Stream.of(ModelItemTypeHelper.compareSingleObjects(value1, value2, StringUtils.EMPTY)) 354 .filter(Optional::isPresent) 355 .map(Optional::get); 356 } 357 } 358 359 public boolean isSimple() 360 { 361 return false; 362 } 363}