001/*
002 *  Copyright 2021 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.transformation.htmledition;
018
019import java.io.InputStream;
020import java.util.Map;
021import java.util.Optional;
022
023import javax.jcr.Node;
024import javax.jcr.RepositoryException;
025
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.cocoon.components.ContextHelper;
029import org.apache.cocoon.environment.ObjectModelHelper;
030import org.apache.cocoon.xml.AttributesImpl;
031import org.apache.commons.lang3.StringUtils;
032import org.xml.sax.Attributes;
033import org.xml.sax.SAXException;
034
035import org.ametys.cms.URIPrefixHandler;
036import org.ametys.cms.data.Resource;
037import org.ametys.cms.data.RichText;
038import org.ametys.cms.repository.Content;
039import org.ametys.cms.transformation.ImageResolverHelper;
040import org.ametys.plugins.repository.AmetysObjectResolver;
041import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData;
042import org.ametys.runtime.model.type.DataContext;
043
044/**
045 * Handler to enhance the rich text attachments.
046 * Add the content id and the data path to the local attachment's attribute
047 */
048public class DocbookLocalMediaObjectEditionHandler extends AbstractHTMLEditionHandler
049{
050    private static final String __ATTACHMENT_IMAGE_TAG_NAME = "imagedata";
051    private static final String __ATTACHMENT_VIDEO_TAG_NAME = "videodata";
052    private static final String __ATTACHMENT_AUDIO_TAG_NAME = "audiodata";
053    private static final String __ATTACHMENT_TYPE_ATTRIBUTE_NAME = "type";
054    private static final String __ATTACHMENT_TYPE_ATTRIBUTE_LOCAL_VALUE = "local";
055
056    /** Ametys object resolver */
057    protected AmetysObjectResolver _resolver;
058    
059    /** the context of the data being edited */
060    protected DataContext _dataContext;
061    
062    private URIPrefixHandler _prefixHandler;
063    
064    @Override
065    public void service(ServiceManager manager) throws ServiceException
066    {
067        super.service(manager);
068        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
069        _prefixHandler = (URIPrefixHandler) manager.lookup(URIPrefixHandler.ROLE);
070    }
071    
072    @Override
073    public void startDocument() throws SAXException
074    {
075        Map objectModel = ContextHelper.getObjectModel(_context);
076        Map parentContextParameters = (Map) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
077        _dataContext = (DataContext) parentContextParameters.get("dataContext");
078        
079        super.startDocument();
080    }
081
082    @Override
083    public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException
084    {
085        // A new attachment starts being saxed
086        boolean isAttachment = _isAttachment(loc);
087        String type = attrs.getValue(__ATTACHMENT_TYPE_ATTRIBUTE_NAME);
088        if (isAttachment && __ATTACHMENT_TYPE_ATTRIBUTE_LOCAL_VALUE.equals(type))
089        {
090            Attributes newAttrs = _processAttachment(attrs);
091            super.startElement(uri, loc, raw, newAttrs);
092        }
093        else
094        {
095            super.startElement(uri, loc, raw, attrs);
096        }
097    }
098
099    private boolean _isAttachment(String loc)
100    {
101        return __ATTACHMENT_IMAGE_TAG_NAME.equals(loc) || __ATTACHMENT_VIDEO_TAG_NAME.equals(loc) || __ATTACHMENT_AUDIO_TAG_NAME.equals(loc);
102    }
103
104    private Attributes _processAttachment(Attributes attrs) throws SAXException
105    {
106        String contentId = _dataContext.getObjectId()
107                .orElseThrow(() -> new SAXException("The object id is required in the data context in order to set the fileref"));
108        Content content = _resolver.resolveById(contentId);
109
110        String dataPath = _dataContext.getDataPath();
111        RichText richText = content.getValue(dataPath);
112        
113        String filename = attrs.getValue("fileref");
114        Resource attachment = richText.getAttachment(filename);
115
116        AttributesImpl newAttrs = new AttributesImpl(attrs);
117        
118        if (attachment != null)
119        {
120            _getAttachmentUUID(attachment)
121                      .map(this::getRichTextByUUIDURL)
122                      .ifPresentOrElse(url -> newAttrs.addCDATAAttribute("resourceByUUID", url),
123                          () -> newAttrs.addCDATAAttribute("base64", _resolveAttachmentAsBase64(attrs, attachment)));
124        }
125
126        return newAttrs;
127    }
128    
129    /**
130     * Retrieves the URL to retrieve a rich text attachment by its UUID 
131     * @param uuid the UUID of the rich text's attachment
132     * @return the URL to retrieve a rich text attachment by its UUID
133     */
134    protected String getRichTextByUUIDURL(String uuid)
135    {
136        StringBuilder resultPath = new StringBuilder();
137        
138        resultPath.append(_prefixHandler.computeUriPrefix(false, false))
139            .append("/plugins/cms/richText-file-by-uuid/")
140            .append(uuid);
141        
142        return resultPath.toString();
143    }
144    
145    private Optional<String> _getAttachmentUUID(Resource attachment)
146    {
147        return attachment.getRepositoryData()
148                .filter(JCRRepositoryData.class::isInstance)
149                .map(JCRRepositoryData.class::cast)
150                .map(JCRRepositoryData::getNode)
151                .map(this::_getNodeIdentifier);
152    }
153    
154    private String _getNodeIdentifier(Node node)
155    {
156        try
157        {
158            return node.getIdentifier();
159        }
160        catch (RepositoryException e)
161        {
162            // Return null to use the fallback method (base64) if we are not able to get the node identifier 
163            return null;
164        }
165    }
166    
167    private String _resolveAttachmentAsBase64(Attributes attrs, Resource attachment)
168    {
169        try (InputStream dataIs = attachment.getInputStream())
170        {
171            String heightAsString = StringUtils.substringBefore(attrs.getValue("depth"), "px");
172            int height = heightAsString != null ? Integer.valueOf(heightAsString) : -1;
173            
174            String widthAsString = StringUtils.substringBefore(attrs.getValue("width"), "px");
175            int width = widthAsString != null ? Integer.valueOf(widthAsString) : -1;
176            
177            return ImageResolverHelper.resolveImageAsBase64(dataIs, attachment.getMimeType(), height, width, 0, 0);
178        }
179        catch (Exception e)
180        {
181            throw new IllegalStateException(e);
182        }
183    }
184}