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}