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.util.ArrayList; 019import java.util.Arrays; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Objects; 024import java.util.Optional; 025import java.util.stream.Collectors; 026 027import org.apache.avalon.framework.service.ServiceException; 028import org.apache.avalon.framework.service.ServiceManager; 029import org.apache.cocoon.xml.AttributesImpl; 030import org.apache.cocoon.xml.XMLUtils; 031import org.apache.commons.lang3.StringUtils; 032import org.w3c.dom.Element; 033import org.xml.sax.ContentHandler; 034import org.xml.sax.SAXException; 035 036import org.ametys.cms.contenttype.ContentTypesHelper; 037import org.ametys.cms.data.ContentValue; 038import org.ametys.cms.data.holder.impl.IndexableDataHolderHelper; 039import org.ametys.cms.repository.Content; 040import org.ametys.cms.repository.ModifiableContent; 041import org.ametys.core.model.type.AbstractElementType; 042import org.ametys.core.util.DateUtils; 043import org.ametys.plugins.repository.AmetysObjectResolver; 044import org.ametys.plugins.repository.AmetysRepositoryException; 045import org.ametys.runtime.model.ViewItem; 046import org.ametys.runtime.model.ViewItemAccessor; 047import org.ametys.runtime.model.exception.BadItemTypeException; 048import org.ametys.runtime.model.type.DataContext; 049 050/** 051 * Abstract class for content type of elements 052 */ 053public abstract class AbstractContentElementType extends AbstractElementType<ContentValue> 054{ 055 /** Ametys object resolver */ 056 protected AmetysObjectResolver _resolver; 057 058 /** Helper for content types */ 059 protected ContentTypesHelper _contentTypesHelper; 060 061 @Override 062 public void service(ServiceManager manager) throws ServiceException 063 { 064 super.service(manager); 065 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 066 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 067 } 068 069 @Override 070 public ContentValue convertValue(Object value) throws BadItemTypeException 071 { 072 if (value == null) 073 { 074 return null; 075 } 076 if (value instanceof ContentValue) 077 { 078 return (ContentValue) value; 079 } 080 else if (value instanceof String) 081 { 082 return new ContentValue(_resolver, (String) value); 083 } 084 else if (value instanceof ModifiableContent) 085 { 086 return new ContentValue((ModifiableContent) value); 087 } 088 089 return super.convertValue(value); 090 } 091 092 @Override 093 public String toString(ContentValue value) 094 { 095 return value.getContentId(); 096 } 097 098 public Object fromJSONForClient(Object json, DataContext context) 099 { 100 if (json == null) 101 { 102 return json; 103 } 104 else if (json instanceof String) 105 { 106 return convertValue(json); 107 } 108 else if (json instanceof List) 109 { 110 @SuppressWarnings("unchecked") 111 List<String> jsonList = (List<String>) json; 112 List<ContentValue> contentList = new ArrayList<>(); 113 for (String singleJSON : jsonList) 114 { 115 contentList.add(castValue(singleJSON)); 116 } 117 return contentList.toArray(new ContentValue[contentList.size()]); 118 } 119 else 120 { 121 throw new IllegalArgumentException("Try to convert the non content JSON object '" + json + "' into a content"); 122 } 123 } 124 125 @Override 126 public Object valueToJSONForClient(Object value, Optional<ViewItem> viewItem, DataContext context) 127 { 128 return _valueToJSON(value, viewItem, context, this::_content2JsonForClient, this::_content2JsonForClient, this::_content2JsonForClient); 129 } 130 131 private Object _content2JsonForClient(ContentValue contentValue, Optional<ViewItem> viewItem, DataContext context) 132 { 133 return _content2JsonForClient(contentValue.getContentId(), viewItem, context); 134 } 135 136 private Object _content2JsonForClient(String contentId, Optional<ViewItem> viewItem, DataContext context) 137 { 138 try 139 { 140 Content content = _resolver.resolveById(contentId); 141 return _content2JsonForClient(content, viewItem, context); 142 } 143 catch (AmetysRepositoryException e) 144 { 145 if (getLogger().isWarnEnabled()) 146 { 147 getLogger().warn(_getLogForNonExistingContent(contentId, "Unable to convert it as JSON.", context), e); 148 } 149 return null; 150 } 151 } 152 153 private Object _content2JsonForClient(Content content, Optional<ViewItem> viewItem, DataContext context) 154 { 155 Map<String, Object> contentProperties = new HashMap<>(); 156 157 contentProperties.put("id", content.getId()); 158 contentProperties.put("title", content.getTitle(context.getLocale())); 159 contentProperties.put("iconGlyph", StringUtils.defaultString(_contentTypesHelper.getIconGlyph(content))); 160 contentProperties.put("iconDecorator", StringUtils.defaultString(_contentTypesHelper.getIconDecorator(content))); 161 contentProperties.put("smallIcon", StringUtils.defaultString(_contentTypesHelper.getSmallIcon(content))); 162 contentProperties.put("mediumIcon", StringUtils.defaultString(_contentTypesHelper.getMediumIcon(content))); 163 contentProperties.put("largeIcon", StringUtils.defaultString(_contentTypesHelper.getLargeIcon(content))); 164 contentProperties.put("contentTypes", content.getTypes()); 165 contentProperties.put("mixins", content.getMixinTypes()); 166 167 if (viewItem.isPresent()) 168 { 169 ViewItem contentViewItem = viewItem.get(); 170 if (contentViewItem instanceof ViewItemAccessor contentViewItemAccessor) 171 { 172 DataContext newContext = context.cloneContext() 173 .withDataPath(StringUtils.EMPTY) 174 .withObjectId(content.getId()); 175 Map<String, Object> values = IndexableDataHolderHelper.dataToJSON(content, contentViewItemAccessor, newContext, false); 176 contentProperties.putAll(values); 177 } 178 } 179 180 return contentProperties; 181 } 182 183 @Override 184 public Object valueToJSONForEdition(Object value, Optional<ViewItem> viewItem, DataContext context) 185 { 186 return _valueToJSON(value, viewItem, context, (string, vI, c) -> string, (content, vI, c) -> content.getId(), (contentValue, vI, c) -> contentValue.getContentId()); 187 } 188 189 /** 190 * Convert the value into a JSON object using the given functions 191 * @param value the value to convert 192 * @param viewItem The optional view item corresponding to the item that is currently converted. 193 * This view item gives context for the JSON conversion. 194 * @param context The context of the data to convert 195 * @param singleStringToJSONFunction the function to apply to each single string to convert it into a JSON object 196 * @param singleContentToJSONFunction the function to apply to each single content to convert it into a JSON object 197 * @param singleContentValueToJSONFunction the function to apply to each single content value to convert it into a JSON object 198 * @return The value as JSON 199 */ 200 protected Object _valueToJSON(Object value, Optional<ViewItem> viewItem, DataContext context, 201 Content2Json<String> singleStringToJSONFunction, 202 Content2Json<Content> singleContentToJSONFunction, 203 Content2Json<ContentValue> singleContentValueToJSONFunction) 204 { 205 if (value == null) 206 { 207 return value; 208 } 209 else if (value instanceof String singleString) 210 { 211 return singleStringToJSONFunction.content2Json(singleString, viewItem, context); 212 } 213 else if (value instanceof String[] multipleString) 214 { 215 return Arrays.stream(multipleString) 216 .map(contentId -> singleStringToJSONFunction.content2Json(contentId, viewItem, context)) 217 .filter(Objects::nonNull) 218 .collect(Collectors.toList()); 219 } 220 else if (value instanceof Content singleContent) 221 { 222 return singleContentToJSONFunction.content2Json(singleContent, viewItem, context); 223 } 224 else if (value instanceof Content[] multipleContent) 225 { 226 return Arrays.stream(multipleContent) 227 .map(content -> singleContentToJSONFunction.content2Json(content, viewItem, context)) 228 .collect(Collectors.toList()); 229 } 230 else if (value instanceof ContentValue singleContentValue) 231 { 232 return singleContentValueToJSONFunction.content2Json(singleContentValue, viewItem, context); 233 } 234 else if (value instanceof ContentValue[] multipleContentValue) 235 { 236 return Arrays.stream(multipleContentValue) 237 .filter(contentValue -> StringUtils.isNotEmpty(contentValue.getContentId())) 238 .map(contentValue -> singleContentValueToJSONFunction.content2Json(contentValue, viewItem, context)) 239 .filter(Objects::nonNull) 240 .collect(Collectors.toList()); 241 } 242 else 243 { 244 throw new IllegalArgumentException("Try to convert the non content value '" + value + "' to JSON"); 245 } 246 } 247 248 @Override 249 protected ContentValue _singleValueFromXML(Element element, Optional<Object> additionalData) 250 { 251 String contentId = element.getAttribute("id"); 252 if (StringUtils.isNotEmpty(contentId)) 253 { 254 return new ContentValue(_resolver, contentId); 255 } 256 257 return null; 258 } 259 260 @Override 261 protected void _valueToSAX(ContentHandler contentHandler, String tagName, Object value, Optional<ViewItem> viewItem, DataContext context, AttributesImpl attributes) throws SAXException 262 { 263 if (value instanceof String) 264 { 265 _singleContentAsStringToSAX(contentHandler, tagName, (String) value, attributes, viewItem, context); 266 } 267 else if (value instanceof String[]) 268 { 269 if (((String[]) value).length <= 0) 270 { 271 XMLUtils.createElement(contentHandler, tagName, attributes); 272 } 273 else 274 { 275 for (String contentId : (String[]) value) 276 { 277 _singleContentAsStringToSAX(contentHandler, tagName, contentId, attributes, viewItem, context); 278 } 279 } 280 } 281 else if (value instanceof Content) 282 { 283 _singleContentToSAX(contentHandler, tagName, (Content) value, attributes, viewItem, context); 284 } 285 else if (value instanceof Content[]) 286 { 287 if (((Content[]) value).length <= 0) 288 { 289 XMLUtils.createElement(contentHandler, tagName, attributes); 290 } 291 else 292 { 293 for (Content content : (Content[]) value) 294 { 295 _singleContentToSAX(contentHandler, tagName, content, attributes, viewItem, context); 296 } 297 } 298 } 299 else if (value instanceof ContentValue) 300 { 301 _singleContentValueToSAX(contentHandler, tagName, (ContentValue) value, attributes, viewItem, context); 302 } 303 else if (value instanceof ContentValue[]) 304 { 305 if (((ContentValue[]) value).length <= 0) 306 { 307 XMLUtils.createElement(contentHandler, tagName, attributes); 308 } 309 else 310 { 311 for (ContentValue contentWrapper : (ContentValue[]) value) 312 { 313 _singleContentValueToSAX(contentHandler, tagName, contentWrapper, attributes, viewItem, context); 314 } 315 } 316 } 317 } 318 319 private void _singleContentValueToSAX(ContentHandler contentHandler, String tagName, ContentValue contentValue, AttributesImpl attributes, Optional<ViewItem> viewItem, DataContext context) throws SAXException 320 { 321 _singleContentAsStringToSAX(contentHandler, tagName, contentValue.getContentId(), attributes, viewItem, context); 322 } 323 324 private void _singleContentAsStringToSAX(ContentHandler contentHandler, String tagName, String contentId, AttributesImpl attributes, Optional<ViewItem> viewItem, DataContext context) throws SAXException 325 { 326 try 327 { 328 Content content = _resolver.resolveById(contentId); 329 _singleContentToSAX(contentHandler, tagName, content, attributes, viewItem, context); 330 } 331 catch (AmetysRepositoryException e) 332 { 333 if (getLogger().isWarnEnabled()) 334 { 335 String error = String.format("Unable to generate SAX event in the tag '%s'.", tagName); 336 getLogger().warn(_getLogForNonExistingContent(contentId, error, context), e); 337 } 338 } 339 } 340 341 private String _getLogForNonExistingContent(String contentId, String error, DataContext context) 342 { 343 List<String> sentences = new ArrayList<>(); 344 345 sentences.add(String.format("The content with id '%s' does not exist.", contentId)); 346 sentences.add(error); 347 348 if (context.getObjectId().isPresent()) 349 { 350 sentences.add(String.format("This content is referenced by the object with id '%s' on data at path '%s'.", context.getObjectId().get(), context.getDataPath())); 351 } 352 353 return StringUtils.join(sentences, " "); 354 } 355 356 private void _singleContentToSAX(ContentHandler contentHandler, String tagName, Content content, AttributesImpl attributes, Optional<ViewItem> viewItem, DataContext context) throws SAXException 357 { 358 AttributesImpl localAttributes = new AttributesImpl(attributes); 359 localAttributes.addCDATAAttribute("id", content.getId()); 360 localAttributes.addCDATAAttribute("name", content.getName()); 361 localAttributes.addCDATAAttribute("title", content.getTitle(context.getLocale())); 362 if (content.getLanguage() != null) 363 { 364 localAttributes.addCDATAAttribute("language", content.getLanguage()); 365 } 366 localAttributes.addCDATAAttribute("createdAt", DateUtils.zonedDateTimeToString(content.getCreationDate())); 367 localAttributes.addCDATAAttribute("creator", content.getCreator().getLogin()); 368 localAttributes.addCDATAAttribute("lastModifiedAt", DateUtils.zonedDateTimeToString(content.getLastModified())); 369 370 XMLUtils.startElement(contentHandler, tagName, localAttributes); 371 372 if (viewItem.isPresent()) 373 { 374 ViewItem contentViewItem = viewItem.get(); 375 if (contentViewItem instanceof ViewItemAccessor contentViewItemAccessor) 376 { 377 DataContext newContext = context.cloneContext() 378 .withDataPath(StringUtils.EMPTY) 379 .withObjectId(content.getId()); 380 IndexableDataHolderHelper.dataToSAX(content, contentHandler, contentViewItemAccessor, newContext, false); 381 } 382 } 383 384 XMLUtils.endElement(contentHandler, tagName); 385 } 386 387 @Override 388 public boolean isCompatible(Object value) 389 { 390 return super.isCompatible(value) 391 || value instanceof String || value instanceof String[] 392 || value instanceof Content || value instanceof Content[]; 393 } 394 395 public boolean isSimple() 396 { 397 return true; 398 } 399 400 @Override 401 protected boolean _useJSONForEdition() 402 { 403 return true; 404 } 405 406 @Override 407 protected boolean _isMultiple(Object value) 408 { 409 return super._isMultiple(value) || value instanceof String[] || value instanceof Content[]; 410 } 411 412 @Override 413 protected boolean _isSingle(Object value) 414 { 415 return super._isSingle(value) || value instanceof String || value instanceof Content; 416 } 417 418 /** 419 * Convert a content (as string, ContentValue or Content) into a JSON object 420 * @param <T> the type of the content to convert 421 */ 422 @FunctionalInterface 423 public interface Content2Json<T> 424 { 425 /** 426 * Convert the given content into a JSON object 427 * @param content the content to convert 428 * @param viewItem The optional view item corresponding to the content that is currently converted. 429 * This view item gives context for the JSON conversion. 430 * @param context the context of the content to convert 431 * @return the content as JSON 432 */ 433 public Object content2Json(T content, Optional<ViewItem> viewItem, DataContext context); 434 } 435}