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.util.ArrayList;
020import java.util.Arrays;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.stream.Collectors;
025import java.util.stream.Stream;
026
027import javax.xml.transform.TransformerException;
028
029import org.apache.avalon.framework.configuration.Configurable;
030import org.apache.avalon.framework.configuration.Configuration;
031import org.apache.avalon.framework.configuration.ConfigurationException;
032import org.apache.avalon.framework.context.ContextException;
033import org.apache.avalon.framework.context.Contextualizable;
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.cocoon.Constants;
037import org.apache.cocoon.environment.Context;
038import org.apache.cocoon.xml.AttributesImpl;
039import org.apache.cocoon.xml.XMLUtils;
040import org.apache.commons.lang3.StringUtils;
041import org.apache.commons.lang3.tuple.ImmutableTriple;
042import org.apache.commons.lang3.tuple.Triple;
043import org.w3c.dom.Element;
044import org.xml.sax.ContentHandler;
045import org.xml.sax.SAXException;
046
047import org.ametys.cms.data.Binary;
048import org.ametys.cms.data.ExplorerFile;
049import org.ametys.cms.data.File;
050import org.ametys.core.model.type.AbstractElementType;
051import org.ametys.core.model.type.ModelItemTypeHelper;
052import org.ametys.core.upload.Upload;
053import org.ametys.core.upload.UploadManager;
054import org.ametys.core.user.CurrentUserProvider;
055import org.ametys.plugins.repository.AmetysObjectResolver;
056import org.ametys.plugins.repository.UnknownAmetysObjectException;
057import org.ametys.plugins.repository.data.holder.values.UntouchedValue;
058import org.ametys.runtime.model.ViewItem;
059import org.ametys.runtime.model.compare.DataChangeType;
060import org.ametys.runtime.model.compare.DataChangeTypeDetail;
061import org.ametys.runtime.model.type.DataContext;
062
063/**
064 * Abstract class for file type of elements
065 */
066public abstract class AbstractFileElementType extends AbstractElementType<File> implements Configurable, Contextualizable
067{
068    /** Type of he file from explorer */
069    protected static final String __EXPLORER_FILE_TYPE = "explorer";
070    
071    /** Ametys object resolver */
072    protected AmetysObjectResolver _resolver;
073    
074    /** Type of the binary */
075    protected String _binaryType;
076    
077    /** {@link UploadManager} */
078    protected UploadManager _uploadManager;
079    
080    /** {@link CurrentUserProvider} */
081    protected CurrentUserProvider _userProvider;
082    
083    /** Coocon's context */
084    protected Context _context;
085    
086    public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException
087    {
088        _context = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
089    }
090    
091    @Override
092    public void service(ServiceManager manager) throws ServiceException
093    {
094        super.service(manager);
095        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
096        _uploadManager = (UploadManager) manager.lookup(UploadManager.ROLE);
097        _userProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
098    }
099    
100    public void configure(Configuration configuration) throws ConfigurationException
101    {
102        Configuration typeNode = configuration.getChild("binary-type", false);
103        if (typeNode != null)
104        {
105            _binaryType = typeNode.getValue(null);
106        }
107        
108        if (StringUtils.isBlank(_binaryType))
109        {
110            throw new ConfigurationException("The configuration of binary type should contain a node 'binary-type' with the type of the binary ('metadata', 'explorer', ...)", configuration);
111        }
112    }
113    
114    @Override
115    public File convertValue(Object value)
116    {
117        if (value instanceof String)
118        {
119            return new ExplorerFile(_resolver, (String) value);
120        }
121        
122        return super.convertValue(value);
123    }
124
125    @Override
126    public String toString(File value)
127    {
128        if (value instanceof ExplorerFile)
129        {
130            return ((ExplorerFile) value).getResourceId();
131        }
132        else
133        {
134            throw new UnsupportedOperationException("Unable to convert a binary to a string value");
135        }
136    }
137    
138    public Object fromJSONForClient(Object json)
139    {
140        if (json == null)
141        {
142            return json;
143        }
144        else if (json instanceof String)
145        {
146            return convertValue(json);
147        }
148        else if (json instanceof Map)
149        {
150            Map data = (Map) json;
151            if (data.isEmpty())
152            {
153                return null;
154            }
155            
156            String type = (String) ((Map) json).get("type");
157            String id = (String) ((Map) json).get("id");
158            if ("explorer".equals(type))
159            {
160                return new ExplorerFile(_resolver, id);
161            }
162            else if ("attribute".equals(type))
163            {
164                if (id.equals(AbstractBinaryElementType.UNTOUCHED))
165                {
166                    return new UntouchedValue();
167                }
168                
169                Upload upload = _uploadManager.getUpload(_userProvider.getUser(), id);
170                return ResourceElementTypeHelper.binaryFromUpload(upload);
171            }
172
173            throw new IllegalArgumentException("Try to convert the non file JSON object '" + json + "' into a file");
174        }
175        else if (json instanceof List)
176        {
177            @SuppressWarnings("unchecked")
178            List<String> jsonList = (List<String>) json;
179            List<File> fileList = new ArrayList<>();
180            for (String singleJSON : jsonList)
181            {
182                fileList.add(castValue(singleJSON));
183            }
184            return fileList.toArray(new File[fileList.size()]);
185        }
186        else
187        {
188            throw new IllegalArgumentException("Try to convert the non file time JSON object '" + json + "' into a file");
189        }
190    }
191    
192    @Override
193    public Object valueToJSONForClient(Object value, DataContext context)
194    {
195        if (value == null)
196        {
197            return value;
198        }
199        else if (value instanceof String)
200        {
201            ExplorerFile file = new ExplorerFile(_resolver, (String) value);
202            return _singleExplorerFileToJSON(file);
203        }
204        else if (value instanceof String[])
205        {
206            return Arrays.asList((String[]) value).stream()
207                                                  .map(fileId -> new ExplorerFile(_resolver, fileId))
208                                                  .map(file -> _singleExplorerFileToJSON(file))
209                                                  .collect(Collectors.toList());
210        }
211        else if (value instanceof ExplorerFile)
212        {
213            return _singleExplorerFileToJSON((ExplorerFile) value);
214        }
215        else if (value instanceof ExplorerFile[])
216        {
217            return Arrays.asList((ExplorerFile[]) value).stream()
218                                                        .map(file -> _singleExplorerFileToJSON(file))
219                                                        .collect(Collectors.toList());
220        }
221        else if (value instanceof Binary)
222        {
223            return ResourceElementTypeHelper.singleBinaryToJSON((Binary) value, _binaryType, context);
224        }
225        else if (value instanceof Binary[])
226        {
227            return Arrays.asList((Binary[]) value).stream()
228                                                  .map(file -> ResourceElementTypeHelper.singleBinaryToJSON(file, _binaryType, context))
229                                                  .collect(Collectors.toList());
230        }
231        else
232        {
233            throw new IllegalArgumentException("Try to convert the non file value '" + value + "' to JSON");
234        }
235    }
236    
237    @Override
238    public boolean isCompatible(Object value)
239    {
240        return super.isCompatible(value) || value instanceof UntouchedValue;
241    }
242    
243    private Object _singleExplorerFileToJSON(ExplorerFile explorerFile)
244    {
245        String explorerFileId = explorerFile.getResourceId();
246        try
247        {
248            Map<String, Object> singleFileToJSON = ResourceElementTypeHelper.singleFileToJSON(explorerFile, __EXPLORER_FILE_TYPE, explorerFileId, explorerFileId);
249            singleFileToJSON.put("id", explorerFileId);
250            
251            return singleFileToJSON;
252        }
253        catch (UnknownAmetysObjectException e)
254        {
255            getLogger().warn("The resource of id '{}' does not exist", explorerFileId, e);
256            return null;
257        }
258    }
259    
260    @Override
261    protected File _singleValueFromXML(Element element, Optional<Object> additionalData) throws TransformerException, IOException
262    {
263        String type = element.getAttribute("type");
264        if (__EXPLORER_FILE_TYPE.equals(type))
265        {
266            String resourceId = element.getAttribute("path");
267            return new ExplorerFile(_resolver, resourceId);
268        }
269        else
270        {
271            Binary binary = new Binary();
272            ResourceElementTypeHelper.resourceFromXML(binary, element, additionalData, _context);
273            return binary;
274        }
275    }
276    
277    @Override
278    protected void _valueToSAX(ContentHandler contentHandler, String tagName, Object value, Optional<ViewItem> viewItem, DataContext context, AttributesImpl attributes) throws SAXException, IOException
279    {
280        AttributesImpl localAttributes = new AttributesImpl(attributes);
281        
282        if (value instanceof String)
283        {
284            ExplorerFile file = new ExplorerFile(_resolver, (String) value);
285            _singleExplorerFileToSAX(contentHandler, tagName, file, attributes);
286        }
287        else if (value instanceof String[])
288        {
289            if (((String[]) value).length <= 0)
290            {
291                XMLUtils.createElement(contentHandler, tagName, localAttributes);
292            }
293            else
294            {
295                for (String fileId : (String[]) value)
296                {
297                    ExplorerFile file = new ExplorerFile(_resolver, fileId);
298                    _singleExplorerFileToSAX(contentHandler, tagName, file, attributes);
299                }
300            }
301        }
302        else if (value instanceof ExplorerFile)
303        {
304            ExplorerFile file = (ExplorerFile) value;
305            _singleExplorerFileToSAX(contentHandler, tagName, file, attributes);
306        }
307        else if (value instanceof ExplorerFile[])
308        {
309            if (((ExplorerFile[]) value).length <= 0)
310            {
311                XMLUtils.createElement(contentHandler, tagName, localAttributes);
312            }
313            else
314            {
315                for (ExplorerFile file : (ExplorerFile[]) value)
316                {
317                    _singleExplorerFileToSAX(contentHandler, tagName, file, attributes);
318                }
319            }
320        }
321        else if (value instanceof Binary)
322        {
323            ResourceElementTypeHelper.singleBinaryToSAX(contentHandler, tagName, (Binary) value, _binaryType, context, localAttributes);
324        }
325        else if (value instanceof Binary[])
326        {
327            if (((Binary[]) value).length <= 0)
328            {
329                XMLUtils.createElement(contentHandler, tagName, localAttributes);
330            }
331            else
332            {
333                for (Binary binary : (Binary[]) value)
334                {
335                    ResourceElementTypeHelper.singleBinaryToSAX(contentHandler, tagName, binary, _binaryType, context, localAttributes);
336                }
337            }
338        }
339    }
340    
341    private void _singleExplorerFileToSAX(ContentHandler contentHandler, String tagName, ExplorerFile explorerFile, AttributesImpl attributes) throws SAXException
342    {
343        String explorerFileId = explorerFile.getResourceId();
344
345        try
346        {
347            
348            AttributesImpl localAttributes = new AttributesImpl(attributes);
349            localAttributes.addCDATAAttribute("id", explorerFileId);
350            
351            ResourceElementTypeHelper.singleFileToSAX(contentHandler, tagName, explorerFile, __EXPLORER_FILE_TYPE, explorerFileId, explorerFileId, localAttributes);
352        }
353        catch (UnknownAmetysObjectException e)
354        {
355            getLogger().warn("The resource of id '{}' does not exist", explorerFileId, e);
356        }
357    }
358    
359    @Override
360    protected Stream<Triple<DataChangeType, DataChangeTypeDetail, String>> _compareMultipleValues(File[] value1, File[] value2) throws IOException
361    {
362        throw new UnsupportedOperationException("Unable to compare multiple values of type '" + getId() + "'");
363    }
364    
365    @Override
366    protected Stream<Triple<DataChangeType, DataChangeTypeDetail, String>> _compareSingleValues(File value1, File value2) throws IOException
367    {
368        if (ModelItemTypeHelper.areSingleObjectsBothNotNullAndDifferents(value1, value2))
369        {
370            if (_havValuesSameFileType(value1, value2))
371            {
372                if (value1 instanceof Binary)
373                {
374                    return ResourceElementTypeHelper.compareSingleBinaries((Binary) value1, (Binary) value2);
375                }
376                else if (value1 instanceof ExplorerFile)
377                {
378                    return Stream.of(ModelItemTypeHelper.compareSingleObjects(((ExplorerFile) value1).getResourceId(), ((ExplorerFile) value2).getResourceId(), StringUtils.EMPTY))
379                                 .filter(Optional::isPresent)
380                                 .map(Optional::get);
381                }
382                else
383                {
384                    return super._compareSingleValues(value1, value2);
385                }
386            }
387            else
388            {
389                return Stream.of(new ImmutableTriple<>(DataChangeType.MODIFIED, DataChangeTypeDetail.TYPE, StringUtils.EMPTY));
390            }
391        }
392        else
393        {
394            return Stream.of(ModelItemTypeHelper.compareSingleObjects(value1, value2, StringUtils.EMPTY))
395                         .filter(Optional::isPresent)
396                         .map(Optional::get);
397        }
398    }
399    
400    private boolean _havValuesSameFileType(File value1, File value2)
401    {
402        return (value1 instanceof ExplorerFile && value2 instanceof ExplorerFile) || (value1 instanceof Binary && value2 instanceof Binary);
403    }
404
405    public boolean isSimple()
406    {
407        return false;
408    }
409    
410    @Override
411    protected boolean _useJSONForEdition()
412    {
413        return true;
414    }
415}