001/*
002 * Copyright (c) 2006-2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Florent Guillaume
011 */
012package org.nuxeo.ecm.core.opencmis.impl.server;
013
014import java.io.IOException;
015import java.io.InputStream;
016import java.io.Serializable;
017import java.math.BigInteger;
018import java.net.URI;
019import java.util.Collections;
020import java.util.GregorianCalendar;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.function.Supplier;
025
026import javax.servlet.http.HttpServletRequest;
027import javax.servlet.http.HttpServletResponse;
028
029import org.apache.chemistry.opencmis.commons.data.CacheHeaderContentStream;
030import org.apache.chemistry.opencmis.commons.data.CmisExtensionElement;
031import org.apache.chemistry.opencmis.commons.data.ContentLengthContentStream;
032import org.apache.chemistry.opencmis.commons.data.ContentStream;
033import org.apache.chemistry.opencmis.commons.data.LastModifiedContentStream;
034import org.apache.chemistry.opencmis.commons.data.RedirectingContentStream;
035import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
036import org.apache.commons.io.input.ClosedInputStream;
037import org.apache.commons.io.input.ProxyInputStream;
038import org.nuxeo.ecm.core.api.Blob;
039import org.nuxeo.ecm.core.api.DocumentModel;
040import org.nuxeo.ecm.core.blob.BlobManager;
041import org.nuxeo.ecm.core.blob.BlobManager.UsageHint;
042import org.nuxeo.ecm.core.io.download.DownloadService;
043import org.nuxeo.runtime.api.Framework;
044
045/**
046 * Nuxeo implementation of a CMIS {@link ContentStream}, backed by a {@link Blob}.
047 */
048public class NuxeoContentStream
049        implements CacheHeaderContentStream, LastModifiedContentStream, ContentLengthContentStream {
050
051    public static long LAST_MODIFIED;
052
053    protected final Blob blob;
054
055    protected final GregorianCalendar lastModified;
056
057    protected final InputStream stream;
058
059    private NuxeoContentStream(Blob blob, GregorianCalendar lastModified) {
060        this.blob = blob;
061        this.lastModified = lastModified;
062        // The callers of getStream() often just want to know if the stream is null or not.
063        // (Callers are ObjectService.GetContentStream / AbstractServiceCall.sendContentStreamHeaders)
064        // Also in case we end up redirecting, we don't want to get the stream (which is possibly costly) to just have
065        // it closed immediately. So we wrap in a lazy implementation
066        stream = new LazyInputStream(this::getActualStream);
067    }
068
069    public static NuxeoContentStream create(DocumentModel doc, String xpath, Blob blob, String reason,
070            Map<String, Serializable> extendedInfos, GregorianCalendar lastModified, HttpServletRequest request) {
071        BlobManager blobManager = Framework.getService(BlobManager.class);
072        URI uri;
073        try {
074            uri = blobManager.getURI(blob, UsageHint.DOWNLOAD, request);
075        } catch (IOException e) {
076            throw new CmisRuntimeException("Failed to get download URI", e);
077        }
078        if (uri != null) {
079            extendedInfos = new HashMap<>(extendedInfos == null ? Collections.emptyMap() : extendedInfos);
080            extendedInfos.put("redirect", uri.toString());
081        }
082        DownloadService downloadService = Framework.getService(DownloadService.class);
083        downloadService.logDownload(doc, xpath, blob.getFilename(), reason, extendedInfos);
084        if (uri == null) {
085            return new NuxeoContentStream(blob, lastModified);
086        } else {
087            return new NuxeoRedirectingContentStream(blob, lastModified, uri.toString());
088        }
089    }
090
091    @Override
092    public long getLength() {
093        return blob.getLength();
094    }
095
096    @Override
097    public BigInteger getBigLength() {
098        return BigInteger.valueOf(blob.getLength());
099    }
100
101    @Override
102    public String getMimeType() {
103        return blob.getMimeType();
104    }
105
106    @Override
107    public String getFileName() {
108        return blob.getFilename();
109    }
110
111    @Override
112    public InputStream getStream() {
113        return stream;
114    }
115
116    protected InputStream getActualStream() {
117        try {
118            return blob.getStream();
119        } catch (IOException e) {
120            throw new CmisRuntimeException("Failed to get stream", e);
121        }
122    }
123
124    @Override
125    public List<CmisExtensionElement> getExtensions() {
126        return null;
127    }
128
129    @Override
130    public void setExtensions(List<CmisExtensionElement> extensions) {
131        throw new UnsupportedOperationException();
132    }
133
134    @Override
135    public String getCacheControl() {
136        return null;
137    }
138
139    @Override
140    public String getETag() {
141        return blob.getDigest();
142    }
143
144    @Override
145    public GregorianCalendar getExpires() {
146        return null;
147    }
148
149    @Override
150    public GregorianCalendar getLastModified() {
151        LAST_MODIFIED = lastModified == null ? 0 : lastModified.getTimeInMillis();
152        return lastModified;
153    }
154
155    /**
156     * An {@link InputStream} that fetches the actual stream from a {@link Supplier} on first use.
157     *
158     * @since 7.10
159     */
160    public static class LazyInputStream extends ProxyInputStream {
161
162        protected Supplier<InputStream> supplier;
163
164        public LazyInputStream(Supplier<InputStream> supplier) {
165            super(null);
166            this.supplier = supplier;
167        }
168
169        @Override
170        protected void beforeRead(int n) {
171            if (in == null) {
172                in = supplier.get();
173                supplier = null;
174            }
175        }
176
177        @Override
178        public void close() throws IOException {
179            if (in == null) {
180                in = new ClosedInputStream();
181                supplier = null;
182                return;
183            }
184            super.close();
185        }
186
187        @Override
188        public long skip(long ln) throws IOException {
189            beforeRead(0);
190            return super.skip(ln);
191        }
192
193        @Override
194        public int available() throws IOException {
195            beforeRead(0);
196            return super.available();
197        }
198
199        @Override
200        public void mark(int readlimit) {
201            beforeRead(0);
202            super.mark(readlimit);
203        }
204
205        @Override
206        public void reset() throws IOException {
207            beforeRead(0);
208            super.reset();
209        }
210
211        @Override
212        public boolean markSupported() {
213            beforeRead(0);
214            return super.markSupported();
215        }
216    }
217
218    /**
219     * A {@link NuxeoContentStream} that will generate a redirect.
220     *
221     * @since 7.10
222     */
223    public static class NuxeoRedirectingContentStream extends NuxeoContentStream implements RedirectingContentStream {
224
225        protected final String location;
226
227        private NuxeoRedirectingContentStream(Blob blob, GregorianCalendar lastModified, String location) {
228            super(blob, lastModified);
229            this.location = location;
230        }
231
232        @Override
233        public int getStatus() {
234            // use same redirect code as HttpServletResponse.sendRedirect
235            return HttpServletResponse.SC_FOUND;
236        }
237
238        @Override
239        public String getLocation() {
240            return location;
241        }
242    }
243
244}