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