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