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}