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