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