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}