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