001/* 002 * Copyright 2020 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.data; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.net.URL; 022import java.util.Map; 023 024import org.apache.avalon.framework.component.Component; 025import org.apache.avalon.framework.context.ContextException; 026import org.apache.avalon.framework.context.Contextualizable; 027import org.apache.cocoon.Constants; 028import org.apache.cocoon.environment.Context; 029import org.apache.cocoon.xml.AttributesImpl; 030import org.apache.excalibur.xml.sax.ContentHandlerProxy; 031import org.xml.sax.Attributes; 032import org.xml.sax.ContentHandler; 033import org.xml.sax.SAXException; 034 035import org.ametys.cms.repository.comment.CommentsDAO; 036 037/** 038 * Factory for the transformer that imports a rich text from docbook. 039 */ 040public class RichTextImportHandlerFactory implements Component, Contextualizable 041{ 042 /** Avalon role. */ 043 public static final String ROLE = RichTextImportHandlerFactory.class.getName(); 044 private Context _cocoonContext; 045 046 public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException 047 { 048 _cocoonContext = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 049 } 050 051 /** 052 * Creates a handler proxy to import the rich text 053 * @param contentHandler the contentHandler to pass SAX events to 054 * @param richText the rich text 055 * @param files the attachments of this rich text 056 * @return the created handler 057 */ 058 public RichTextImportHandler createHandlerProxy(ContentHandler contentHandler, RichText richText, Map<String, InputStream> files) 059 { 060 return new RichTextImportHandler(contentHandler, richText, files); 061 } 062 063 /** 064 * This transformer imports the rich text from docbook. 065 */ 066 public class RichTextImportHandler extends ContentHandlerProxy 067 { 068 private static final String __ATTACHMENT_IMAGE_TAG_NAME = "imagedata"; 069 private static final String __ATTACHMENT_VIDEO_TAG_NAME = "videodata"; 070 private static final String __ATTACHMENT_AUDIO_TAG_NAME = "audiodata"; 071 private static final String __ATTACHMENT_TYPE_ATTRIBUTE_NAME = "type"; 072 private static final String __ATTACHMENT_TYPE_ATTRIBUTE_LOCAL_VALUE = "local"; 073 074 private static final String __ANNOTATION_TAG_NAME = "phrase"; 075 private static final String __ANNOTATION_NAME_ATTRIBUTE_NAME = "role"; 076 private static final String __ANNOTATION_CLASS_ATTRIBUTE_NAME = "class"; 077 private static final String __ANNOTATION_CLASS_ATTRIBUTE_VALUE = "semantic"; 078 079 080 private RichText _richText; 081 private Map<String, InputStream> _files; 082 083 private boolean _isCurrentlyInAnnotation; 084 private String _currentAnnotationName; 085 private StringBuilder _currentAnnotationValue; 086 private int _cptrElementsInsideCurrentAnnotation; 087 088 /** 089 * Creates a handler proxy to import a rich text 090 * @param contentHandler the contentHandler to pass SAX events to 091 * @param richText the rich text 092 * @param files the attachments of this rich text 093 */ 094 public RichTextImportHandler(ContentHandler contentHandler, RichText richText, Map<String, InputStream> files) 095 { 096 super(contentHandler); 097 _richText = richText; 098 _files = files; 099 } 100 101 @Override 102 public void startDocument() throws SAXException 103 { 104 // Remove all existing attachments from the rich text. 105 _richText.removeAttachments(); 106 107 // Remove all existing annotations from the rich text. 108 _richText.removeAllAnnotations(); 109 110 super.startDocument(); 111 } 112 113 @Override 114 public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException 115 { 116 // A new attachment starts being saxed 117 boolean isAttachment = _isAttachment(loc); 118 String type = attrs.getValue(__ATTACHMENT_TYPE_ATTRIBUTE_NAME); 119 Attributes newAttrs = attrs; 120 if (isAttachment && __ATTACHMENT_TYPE_ATTRIBUTE_LOCAL_VALUE.equals(type)) 121 { 122 newAttrs = _processAttachment(attrs); 123 } 124 125 // A new semantic annotation starts being saxed 126 String clazz = attrs.getValue(__ANNOTATION_CLASS_ATTRIBUTE_NAME); 127 String annotationName = attrs.getValue(__ANNOTATION_NAME_ATTRIBUTE_NAME); 128 if (__ANNOTATION_TAG_NAME.equals(loc) && __ANNOTATION_CLASS_ATTRIBUTE_VALUE.equals(clazz) && annotationName != null) 129 { 130 _processAnnotation(attrs); 131 } 132 else if (_isCurrentlyInAnnotation) 133 { 134 // A new element is being SAXed inside the current annotation 135 _cptrElementsInsideCurrentAnnotation++; 136 } 137 138 super.startElement(uri, loc, raw, newAttrs); 139 } 140 141 private boolean _isAttachment(String loc) 142 { 143 return __ATTACHMENT_IMAGE_TAG_NAME.equals(loc) || __ATTACHMENT_VIDEO_TAG_NAME.equals(loc) || __ATTACHMENT_AUDIO_TAG_NAME.equals(loc); 144 } 145 146 private Attributes _processAttachment(Attributes attrs) throws SAXException 147 { 148 String fileRefAttribute = attrs.getValue("fileref"); 149 String filename; 150 151 if (CommentsDAO.URL_VALIDATOR.matcher(fileRefAttribute).matches()) 152 { 153 try 154 { 155 NamedResource attachment = new NamedResource(); 156 157 URL url = new URL(fileRefAttribute); 158 try (InputStream is = url.openStream()) 159 { 160 attachment.setInputStream(is); 161 } 162 String path = url.getPath(); 163 filename = path.substring(path.lastIndexOf("/") + 1); 164 165 String mimeType = _cocoonContext.getMimeType(filename.toLowerCase()); 166 attachment.setMimeType(mimeType); 167 attachment.setFilename(filename); 168 _richText.addAttachment(attachment); 169 } 170 catch (IOException e) 171 { 172 throw new SAXException("Unable to process the attachment '" + fileRefAttribute + "'. An error occured while setting its content", e); 173 } 174 } 175 else 176 { 177 // file reference is of the form ownerId@dataName;fileName 178 int indexOfFilenameSeparator = fileRefAttribute.lastIndexOf(';'); 179 filename = fileRefAttribute.substring(indexOfFilenameSeparator + 1); 180 181 if (indexOfFilenameSeparator == -1) 182 { 183 throw new IllegalArgumentException("A local image should have a file reference of the form <protocol>://<protocol-specific-part>;<filename> : " + fileRefAttribute); 184 } 185 186 if (_files.containsKey(filename)) 187 { 188 try 189 { 190 NamedResource attachment = new NamedResource(); 191 String mimeType = _cocoonContext.getMimeType(filename.toLowerCase()); 192 attachment.setMimeType(mimeType); 193 attachment.setFilename(filename); 194 attachment.setInputStream(_files.get(filename)); 195 _richText.addAttachment(attachment); 196 } 197 catch (IOException e) 198 { 199 throw new SAXException("Unable to process the attachment '" + filename + "'. An error occured while setting its content", e); 200 } 201 } 202 } 203 204 AttributesImpl newAttrs = new AttributesImpl(); 205 _copyAttributes(attrs, newAttrs); 206 newAttrs.addCDATAAttribute("fileref", filename); 207 return newAttrs; 208 } 209 210 /** 211 * Copy the attributes except the fileref attribute 212 * @param attrs the attributes to copy. 213 * @param newAttrs the attributes to copy to. 214 */ 215 private void _copyAttributes(Attributes attrs, AttributesImpl newAttrs) 216 { 217 for (int i = 0; i < attrs.getLength(); i++) 218 { 219 String name = attrs.getQName(i); 220 221 if (!"fileref".equals(name)) 222 { 223 newAttrs.addAttribute(attrs.getURI(i), attrs.getLocalName(i), name, attrs.getType(i), attrs.getValue(i)); 224 } 225 } 226 } 227 228 private void _processAnnotation(Attributes attrs) 229 { 230 _isCurrentlyInAnnotation = true; 231 _currentAnnotationName = attrs.getValue(__ANNOTATION_NAME_ATTRIBUTE_NAME); 232 _currentAnnotationValue = new StringBuilder(); 233 _cptrElementsInsideCurrentAnnotation = 0; 234 } 235 236 @Override 237 public void characters(char[] ch, int start, int length) throws SAXException 238 { 239 if (_isCurrentlyInAnnotation) 240 { 241 _currentAnnotationValue.append(ch, start, length); 242 } 243 244 super.characters(ch, start, length); 245 } 246 247 @Override 248 public void endElement(String uri, String loc, String raw) throws SAXException 249 { 250 if (_isCurrentlyInAnnotation) 251 { 252 if (_cptrElementsInsideCurrentAnnotation == 0) 253 { 254 // When the semantic annotation is fully saxed, add it to the rich text 255 _richText.addAnnotations(_currentAnnotationName, _currentAnnotationValue.toString()); 256 _isCurrentlyInAnnotation = false; 257 } 258 else 259 { 260 _cptrElementsInsideCurrentAnnotation--; 261 } 262 } 263 264 super.endElement(uri, loc, raw); 265 } 266 } 267}