001/*
002 *  Copyright 2010 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.transformation;
017
018import java.io.InputStream;
019import java.util.Arrays;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.Iterator;
023import java.util.Map;
024
025import org.apache.avalon.framework.context.Context;
026import org.apache.avalon.framework.context.ContextException;
027import org.apache.avalon.framework.context.Contextualizable;
028import org.apache.avalon.framework.logger.AbstractLogEnabled;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.cocoon.components.ContextHelper;
033import org.apache.cocoon.environment.Request;
034
035import org.ametys.cms.repository.Content;
036import org.ametys.cms.transformation.ConsistencyChecker.CHECK;
037import org.ametys.core.util.URLEncoder;
038import org.ametys.plugins.repository.AmetysObjectResolver;
039import org.ametys.plugins.repository.metadata.CompositeMetadata;
040import org.ametys.plugins.repository.metadata.File;
041import org.ametys.plugins.repository.metadata.Folder;
042import org.ametys.plugins.repository.metadata.Resource;
043import org.ametys.plugins.repository.metadata.RichText;
044import org.ametys.plugins.repository.metadata.UnknownMetadataException;
045import org.ametys.plugins.repository.version.VersionableAmetysObject;
046import org.ametys.runtime.i18n.I18nizableText;
047import org.ametys.runtime.workspace.WorkspaceMatcher;
048
049/**
050 * {@link URIResolver} for resources local to a Content.
051 */
052public class LocalURIResolver extends AbstractLogEnabled implements URIResolver, Contextualizable, Serviceable
053{
054    /** The context */
055    protected Context _context;
056    /** The ametys object resolver */
057    protected AmetysObjectResolver _ametysObjectResolver;
058
059    @Override
060    public void service(ServiceManager manager) throws ServiceException
061    {
062        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
063    }
064    
065    @Override
066    public void contextualize(Context context) throws ContextException
067    {
068        _context = context;
069    }
070    
071    @Override
072    public String getType()
073    {
074        return "local";
075    }
076
077    @Override
078    public String resolve(String uri, boolean download, boolean absolute, boolean internal)
079    {
080        // uri are like content://UUID@metadata;data/file.ext
081        int i = uri.indexOf('@');
082        int j = uri.indexOf(';', i);
083        String id = uri.substring(0, i);
084        String metadata = uri.substring(i + 1, j);
085        String path = uri.substring(j + 1);
086
087        try
088        {
089            Request request = ContextHelper.getRequest(_context);
090            
091            String contentVersion = null;
092            Content content = (Content) request.getAttribute(Content.class.getName());
093            if (content != null && id.equals(content.getId()))
094            {
095                contentVersion = ((VersionableAmetysObject) content).getRevision();
096            }
097            
098            StringBuilder resultPath = new StringBuilder();
099            
100            resultPath.append(getUriPrefix(download, absolute, internal))
101                .append("/plugins/cms" + (download ? "/download/" : "/view/"))
102                .append(path);
103            
104            Map<String, String> params = new HashMap<>();
105            params.put("contentId", id);
106            params.put("metadata", metadata);
107            if (contentVersion != null)
108            {
109                params.put("contentVersion", contentVersion);
110            }
111            
112            // Encode twice
113            String encodedPath = URLEncoder.encodePath(resultPath.toString());
114            return URLEncoder.encodeURI(encodedPath, params);
115        }
116        catch (Exception e)
117        {
118            throw new IllegalStateException(e);
119        }
120    }
121    
122    /**
123     * Get the URI prefix
124     * @param download true if the pointed resource is to be downloaded.
125     * @param absolute true if the url must be absolute
126     * @param internal true to get an internal URI.
127     * @return the URI prefix
128     */
129    protected String getUriPrefix (boolean download, boolean absolute, boolean internal)
130    {
131        if (internal)
132        {
133            return "cocoon://";
134        }
135        else 
136        {
137            Request request = ContextHelper.getRequest(_context);
138            String workspaceURI = (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_URI);
139            String uriPrefix = request.getContextPath() + workspaceURI;
140            
141            if (absolute && !uriPrefix.startsWith(request.getScheme()))
142            {
143                uriPrefix = request.getScheme() + "://" + request.getServerName() + (request.getServerPort() != 80 ? ":" + request.getServerPort() : "") + uriPrefix;
144            }
145            
146            return uriPrefix;
147        }
148    }
149    
150    @Override
151    public String resolveImage(String uri, int height, int width, boolean download, boolean absolute, boolean internal)
152    {
153        return resolve(uri, download, absolute, internal);
154    }
155    
156    @Override
157    public String resolveImageAsBase64(String uri, int height, int width)
158    {
159        return resolveImageAsBase64(uri, height, width, 0, 0, 0, 0);
160    }
161    
162    @Override
163    public String resolveBoundedImage(String uri, int maxHeight, int maxWidth, boolean download, boolean absolute, boolean internal)
164    {
165        return resolve(uri, download, absolute, internal);
166    }
167    
168    @Override
169    public String resolveBoundedImageAsBase64(String uri, int maxHeight, int maxWidth)
170    {
171        return resolveImageAsBase64(uri, 0, 0, maxHeight, maxWidth, 0, 0);
172    }
173    
174    @Override
175    public String resolveCroppedImage(String uri, int cropHeight, int cropWidth, boolean download, boolean absolute, boolean internal)
176    {
177        return resolve(uri, download, absolute, internal);
178    }
179    
180    @Override
181    public String resolveCroppedImageAsBase64(String uri, int cropHeight, int cropWidth)
182    {
183        return resolveImageAsBase64(uri, 0, 0, 0, 0, cropHeight, cropWidth);
184    }
185    
186    /**
187     * Get an image's bytes encoded as base64, optionally resized. 
188     * @param uri the image URI.
189     * @param height the specified height. Ignored if negative.
190     * @param width the specified width. Ignored if negative.
191     * @param maxHeight the maximum image height. Ignored if height or width is specified.
192     * @param maxWidth the maximum image width. Ignored if height or width is specified.
193     * @param cropHeight The cropping height. Ignored if negative.
194     * @param cropWidth The cropping width. Ignored if negative.
195     * @return the image bytes encoded as base64.
196     */
197    protected String resolveImageAsBase64(String uri, int height, int width, int maxHeight, int maxWidth, int cropHeight, int cropWidth)
198    {
199        // uri are like content://UUID@metadata;data/file.ext
200        int i = uri.indexOf('@');
201        int j = uri.indexOf(';', i);
202        String id = uri.substring(0, i);
203        String metadata = uri.substring(i + 1, j);
204        String path = uri.substring(j + 1);
205        
206        try
207        {
208            Request request = ContextHelper.getRequest(_context);
209            
210            Content content = (Content) request.getAttribute(Content.class.getName());
211            if (!content.getId().equals(id))
212            {
213                content = _ametysObjectResolver.resolveById(id);
214            }
215            
216            RichText richText = _getMeta(content.getMetadataHolder(), metadata);
217            File file = _getFile(richText.getAdditionalDataFolder(), path);
218            Resource resource = file.getResource();
219            
220            try (InputStream dataIs = resource.getInputStream())
221            {
222                return ImageResolverHelper.resolveImageAsBase64(dataIs, resource.getMimeType(), height, width, maxHeight, maxWidth, cropHeight, cropWidth);
223            }
224        }
225        catch (Exception e)
226        {
227            throw new IllegalStateException(e);
228        }
229    }
230    
231    @Override
232    public CHECK checkLink(String uri, boolean shortTest)
233    {
234        try
235        {
236            int i = uri.indexOf('@');
237            int j = uri.indexOf(';', i);
238            
239            if (i == -1 || j == -1)
240            {
241                getLogger().warn("Failed to check local URI: '" + uri + " does not respect the excepted format 'content://UUID@metadata;data/file.ext'");
242                return CHECK.SERVER_ERROR;
243            }
244            
245            String id = uri.substring(0, i);
246            String metadata = uri.substring(i + 1, j);
247            String fileName = uri.substring(j + 1);
248            
249            Content content = _ametysObjectResolver.resolveById(id);
250            RichText richText = _getMeta(content.getMetadataHolder(), metadata);
251            
252            richText.getAdditionalDataFolder().getFile(fileName);
253            
254            return CHECK.SUCCESS;
255        }
256        catch (UnknownMetadataException e)
257        {
258            return CHECK.NOT_FOUND;
259        }
260        catch (Exception e)
261        {
262            throw new RuntimeException("Cannot check the uri '" + uri + "'", e);
263        }
264    }
265    
266    @Override
267    public I18nizableText getLabel(String uri)
268    {
269        int i = uri.indexOf('@');
270        int j = uri.indexOf(';', i);
271        
272        String fileName = uri.substring(j + 1);
273        
274        return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_LOCAL_LABEL", Collections.singletonList(fileName));
275    }
276    
277    /**
278     * Get the rich text meta
279     * @param meta The composite meta
280     * @param metadataName The metadata name (with /)
281     * @return The rich text meta
282     */
283    protected RichText _getMeta(CompositeMetadata meta, String metadataName)
284    {
285        int pos = metadataName.indexOf("/");
286        if (pos == -1)
287        {
288            return meta.getRichText(metadataName);
289        }
290        else
291        {
292            return _getMeta(meta.getCompositeMetadata(metadataName.substring(0, pos)), metadataName.substring(pos + 1));
293        }
294    }
295    
296    /**
297     * Get the file at the specified path in the given folder.
298     * @param folder The folder to search in.
299     * @param path The file path in the folder (can contain slashes).
300     * @return The file if found, null otherwise.
301     */
302    protected File _getFile(Folder folder, String path)
303    {
304        File file = null;
305        
306        Iterator<String> it = Arrays.asList(path.split("/")).iterator();
307        
308        Folder browsedFolder = folder;
309        while (it.hasNext())
310        {
311            String pathElement = it.next();
312            
313            if (it.hasNext())
314            {
315                // not the last segment : it is a composite
316                browsedFolder = browsedFolder.getFolder(pathElement);
317            }
318            else
319            {
320                file = browsedFolder.getFile(pathElement);
321            }
322        }
323        
324        return file;
325    }
326}