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.core.util.ImageResolverHelper;
040import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData;
041import org.ametys.plugins.repository.model.RepositoryDataContext;
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    /** the context of the data being edited */
057    protected DataContext _dataContext;
058    
059    private URIPrefixHandler _prefixHandler;
060    
061    @Override
062    public void service(ServiceManager manager) throws ServiceException
063    {
064        super.service(manager);
065        _prefixHandler = (URIPrefixHandler) manager.lookup(URIPrefixHandler.ROLE);
066    }
067    
068    @Override
069    public void startDocument() throws SAXException
070    {
071        Map objectModel = ContextHelper.getObjectModel(_context);
072        Map parentContextParameters = (Map) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
073        _dataContext = (DataContext) parentContextParameters.get("dataContext");
074        
075        super.startDocument();
076    }
077
078    @Override
079    public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException
080    {
081        // A new attachment starts being saxed
082        boolean isAttachment = _isAttachment(loc);
083        String type = attrs.getValue(__ATTACHMENT_TYPE_ATTRIBUTE_NAME);
084        if (isAttachment && __ATTACHMENT_TYPE_ATTRIBUTE_LOCAL_VALUE.equals(type))
085        {
086            Attributes newAttrs = _processAttachment(attrs);
087            super.startElement(uri, loc, raw, newAttrs);
088        }
089        else
090        {
091            super.startElement(uri, loc, raw, attrs);
092        }
093    }
094
095    private boolean _isAttachment(String loc)
096    {
097        return __ATTACHMENT_IMAGE_TAG_NAME.equals(loc) || __ATTACHMENT_VIDEO_TAG_NAME.equals(loc) || __ATTACHMENT_AUDIO_TAG_NAME.equals(loc);
098    }
099
100    private Attributes _processAttachment(Attributes attrs) throws SAXException
101    {
102        RepositoryDataContext repoContext = _dataContext instanceof RepositoryDataContext rc ? rc : RepositoryDataContext.newInstance(_dataContext);
103        Content content = (Content) repoContext.getObject()
104                .orElseThrow(() -> new SAXException("The object id is required in the data context in order to set the fileref"));
105            
106        String dataPath = _dataContext.getDataPath();
107        RichText richText = content.getValue(dataPath);
108        
109        String filename = attrs.getValue("fileref");
110        Resource attachment = richText.getAttachment(filename);
111
112        AttributesImpl newAttrs = new AttributesImpl(attrs);
113        
114        if (attachment != null)
115        {
116            _getAttachmentUUID(attachment)
117                      .map(this::getRichTextByUUIDURL)
118                      .ifPresentOrElse(url -> newAttrs.addCDATAAttribute("resourceByUUID", url),
119                          () -> newAttrs.addCDATAAttribute("base64", _resolveAttachmentAsBase64(attrs, attachment)));
120        }
121
122        return newAttrs;
123    }
124    
125    /**
126     * Retrieves the URL to retrieve a rich text attachment by its UUID 
127     * @param uuid the UUID of the rich text's attachment
128     * @return the URL to retrieve a rich text attachment by its UUID
129     */
130    protected String getRichTextByUUIDURL(String uuid)
131    {
132        StringBuilder resultPath = new StringBuilder();
133        
134        resultPath.append(_prefixHandler.computeUriPrefix(false, false))
135            .append("/plugins/cms/richText-file-by-uuid/")
136            .append(uuid);
137        
138        return resultPath.toString();
139    }
140    
141    private Optional<String> _getAttachmentUUID(Resource attachment)
142    {
143        return attachment.getRepositoryData()
144                .filter(JCRRepositoryData.class::isInstance)
145                .map(JCRRepositoryData.class::cast)
146                .map(JCRRepositoryData::getNode)
147                .map(this::_getNodeIdentifier);
148    }
149    
150    private String _getNodeIdentifier(Node node)
151    {
152        try
153        {
154            return node.getIdentifier();
155        }
156        catch (RepositoryException e)
157        {
158            // Return null to use the fallback method (base64) if we are not able to get the node identifier 
159            return null;
160        }
161    }
162    
163    private String _resolveAttachmentAsBase64(Attributes attrs, Resource attachment)
164    {
165        try (InputStream dataIs = attachment.getInputStream())
166        {
167            String heightAsString = StringUtils.substringBefore(attrs.getValue("depth"), "px");
168            int height = heightAsString != null ? Integer.valueOf(heightAsString) : -1;
169            
170            String widthAsString = StringUtils.substringBefore(attrs.getValue("width"), "px");
171            int width = widthAsString != null ? Integer.valueOf(widthAsString) : -1;
172            
173            return ImageResolverHelper.resolveImageAsBase64(dataIs, attachment.getMimeType(), height, width, 0, 0);
174        }
175        catch (Exception e)
176        {
177            throw new IllegalStateException(e);
178        }
179    }
180}