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