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