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