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}