diff --git a/src/main/java/org/alfresco/repo/web/scripts/BufferedRequest.java b/src/main/java/org/alfresco/repo/web/scripts/BufferedRequest.java index 5a8fd56eff..a655e2b631 100644 --- a/src/main/java/org/alfresco/repo/web/scripts/BufferedRequest.java +++ b/src/main/java/org/alfresco/repo/web/scripts/BufferedRequest.java @@ -23,368 +23,383 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.repo.web.scripts; - +package org.alfresco.repo.web.scripts; + import java.io.BufferedReader; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import org.apache.chemistry.opencmis.commons.server.TempStoreOutputStream; -import org.apache.chemistry.opencmis.server.shared.TempStoreOutputStreamFactory; import org.springframework.extensions.surf.util.Content; import org.springframework.extensions.webscripts.Description.FormatStyle; import org.springframework.extensions.webscripts.Match; import org.springframework.extensions.webscripts.Runtime; import org.springframework.extensions.webscripts.WebScriptRequest; import org.springframework.extensions.webscripts.WrappingWebScriptRequest; -import org.springframework.util.FileCopyUtils; - -public class BufferedRequest implements WrappingWebScriptRequest -{ - private TempStoreOutputStreamFactory streamFactory; - private WebScriptRequest req; - private File requestBody; - private InputStream contentStream; - private BufferedReader contentReader; - - public BufferedRequest(WebScriptRequest req, TempStoreOutputStreamFactory streamFactory) - { - this.req = req; - this.streamFactory = streamFactory; - } - - private InputStream bufferInputStream() throws IOException - { - TempStoreOutputStream bufferStream = streamFactory.newOutputStream(); - - try - { - FileCopyUtils.copy(req.getContent().getInputStream(), bufferStream); - } - catch (IOException e) - { - bufferStream.destroy(e); // remove temp file - throw e; - } - - return bufferStream.getInputStream(); - } - - public void reset() - { - if (contentStream != null) - { - try - { - contentStream.close(); - } - catch (Exception e) - { - } - contentStream = null; - } - if (contentReader != null) - { - try - { - contentReader.close(); - } - catch (Exception e) - { - } - contentReader = null; - } - } - - public void close() - { - reset(); - if (requestBody != null) - { - try - { - requestBody.delete(); - } - catch (Exception e) - { - } - requestBody = null; - } - } - - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WrappingWebScriptRequest#getNext() - */ - @Override - public WebScriptRequest getNext() - { - return req; - } - - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#forceSuccessStatus() - */ - @Override - public boolean forceSuccessStatus() - { - return req.forceSuccessStatus(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getAgent() - */ - @Override - public String getAgent() - { - return req.getAgent(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getContent() - */ - @Override - public Content getContent() - { - final Content wrapped = req.getContent(); - return new Content(){ - - @Override - public String getContent() throws IOException - { - return wrapped.getContent(); - } - - @Override - public String getEncoding() - { - return wrapped.getEncoding(); - } - - @Override - public String getMimetype() - { - return wrapped.getMimetype(); - } - - - @Override - public long getSize() - { - return wrapped.getSize(); - } - - @Override - public InputStream getInputStream() - { - if (BufferedRequest.this.contentReader != null) - { - throw new IllegalStateException("Reader in use"); - } - if (BufferedRequest.this.contentStream == null) - { - try - { - BufferedRequest.this.contentStream = bufferInputStream(); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - } - return BufferedRequest.this.contentStream; - } - - @Override - public BufferedReader getReader() throws IOException - { - if (BufferedRequest.this.contentStream != null) - { - throw new IllegalStateException("Input Stream in use"); - } - if (BufferedRequest.this.contentReader == null) - { - String encoding = wrapped.getEncoding(); - InputStream in = bufferInputStream(); - BufferedRequest.this.contentReader = new BufferedReader(new InputStreamReader(in, encoding == null ? "ISO-8859-1" : encoding)); - } - return BufferedRequest.this.contentReader; - } - }; - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getContentType() - */ - @Override - public String getContentType() - { - return req.getContentType(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getContextPath() - */ - @Override - public String getContextPath() - { - return req.getContextPath(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getExtensionPath() - */ - @Override - public String getExtensionPath() - { - return req.getExtensionPath(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getFormat() - */ - @Override - public String getFormat() - { - return req.getFormat(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getFormatStyle() - */ - @Override - public FormatStyle getFormatStyle() - { - return req.getFormatStyle(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getHeader(java.lang.String) - */ - @Override - public String getHeader(String name) - { - return req.getHeader(name); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getHeaderNames() - */ - @Override - public String[] getHeaderNames() - { - return req.getHeaderNames(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getHeaderValues(java.lang.String) - */ - @Override - public String[] getHeaderValues(String name) - { - return req.getHeaderValues(name); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getJSONCallback() - */ - @Override - public String getJSONCallback() - { - return req.getJSONCallback(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getParameter(java.lang.String) - */ - @Override - public String getParameter(String name) - { - return req.getParameter(name); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getParameterNames() - */ - @Override - public String[] getParameterNames() - { - return req.getParameterNames(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getParameterValues(java.lang.String) - */ - @Override - public String[] getParameterValues(String name) - { - return req.getParameterValues(name); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getPathInfo() - */ - @Override - public String getPathInfo() - { - return req.getPathInfo(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getQueryString() - */ - @Override - public String getQueryString() - { - return req.getQueryString(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getRuntime() - */ - @Override - public Runtime getRuntime() - { - return req.getRuntime(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getServerPath() - */ - @Override - public String getServerPath() - { - return req.getServerPath(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getServiceContextPath() - */ - @Override - public String getServiceContextPath() - { - return req.getServiceContextPath(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getServiceMatch() - */ - @Override - public Match getServiceMatch() - { - return req.getServiceMatch(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getServicePath() - */ - @Override - public String getServicePath() - { - return req.getServicePath(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#getURL() - */ - @Override - public String getURL() - { - return req.getURL(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#isGuest() - */ - @Override - public boolean isGuest() - { - return req.isGuest(); - } - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptRequest#parseContent() - */ - @Override - public Object parseContent() - { - return req.parseContent(); - } -} +import org.springframework.util.FileCopyUtils; + +public class BufferedRequest implements WrappingWebScriptRequest +{ + private TempOutputStreamFactory streamFactory; + private WebScriptRequest req; + private TempOutputStream bufferStream; + private InputStream contentStream; + private BufferedReader contentReader; + + public BufferedRequest(WebScriptRequest req, TempOutputStreamFactory streamFactory) + { + this.req = req; + this.streamFactory = streamFactory; + } + + private TempOutputStream getBufferedBodyAsTempStream() throws IOException + { + if (bufferStream == null) + { + bufferStream = streamFactory.createOutputStream(); + + try + { + // Copy the stream + FileCopyUtils.copy(req.getContent().getInputStream(), bufferStream); + } + catch (IOException e) + { + bufferStream.destroy(); + throw e; + } + } + + return bufferStream; + } + + private InputStream bufferInputStream() throws IOException + { + if (contentReader != null) + { + throw new IllegalStateException("Reader in use"); + } + if (contentStream == null) + { + contentStream = getBufferedBodyAsTempStream().getInputStream(); + } + + return contentStream; + } + + public void reset() + { + if (contentStream != null) + { + try + { + contentStream.close(); + } + catch (Exception e) + { + } + contentStream = null; + } + if (contentReader != null) + { + try + { + contentReader.close(); + } + catch (Exception e) + { + } + contentReader = null; + } + } + + public void close() + { + reset(); + if (bufferStream != null) + { + try + { + bufferStream.destroy(); + } + catch (Exception e) + { + } + bufferStream = null; + } + } + + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WrappingWebScriptRequest#getNext() + */ + @Override + public WebScriptRequest getNext() + { + return req; + } + + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#forceSuccessStatus() + */ + @Override + public boolean forceSuccessStatus() + { + return req.forceSuccessStatus(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getAgent() + */ + @Override + public String getAgent() + { + return req.getAgent(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getContent() + */ + @Override + public Content getContent() + { + final Content wrapped = req.getContent(); + return new Content(){ + + @Override + public String getContent() throws IOException + { + return wrapped.getContent(); + } + + @Override + public String getEncoding() + { + return wrapped.getEncoding(); + } + + @Override + public String getMimetype() + { + return wrapped.getMimetype(); + } + + + @Override + public long getSize() + { + return wrapped.getSize(); + } + + @Override + public InputStream getInputStream() + { + if (BufferedRequest.this.contentReader != null) + { + throw new IllegalStateException("Reader in use"); + } + if (BufferedRequest.this.contentStream == null) + { + try + { + BufferedRequest.this.contentStream = bufferInputStream(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + return BufferedRequest.this.contentStream; + } + + @Override + public BufferedReader getReader() throws IOException + { + if (BufferedRequest.this.contentStream != null) + { + throw new IllegalStateException("Input Stream in use"); + } + if (BufferedRequest.this.contentReader == null) + { + String encoding = wrapped.getEncoding(); + InputStream in = bufferInputStream(); + BufferedRequest.this.contentReader = new BufferedReader(new InputStreamReader(in, encoding == null ? "ISO-8859-1" : encoding)); + } + return BufferedRequest.this.contentReader; + } + }; + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getContentType() + */ + @Override + public String getContentType() + { + return req.getContentType(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getContextPath() + */ + @Override + public String getContextPath() + { + return req.getContextPath(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getExtensionPath() + */ + @Override + public String getExtensionPath() + { + return req.getExtensionPath(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getFormat() + */ + @Override + public String getFormat() + { + return req.getFormat(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getFormatStyle() + */ + @Override + public FormatStyle getFormatStyle() + { + return req.getFormatStyle(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getHeader(java.lang.String) + */ + @Override + public String getHeader(String name) + { + return req.getHeader(name); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getHeaderNames() + */ + @Override + public String[] getHeaderNames() + { + return req.getHeaderNames(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getHeaderValues(java.lang.String) + */ + @Override + public String[] getHeaderValues(String name) + { + return req.getHeaderValues(name); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getJSONCallback() + */ + @Override + public String getJSONCallback() + { + return req.getJSONCallback(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getParameter(java.lang.String) + */ + @Override + public String getParameter(String name) + { + return req.getParameter(name); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getParameterNames() + */ + @Override + public String[] getParameterNames() + { + return req.getParameterNames(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getParameterValues(java.lang.String) + */ + @Override + public String[] getParameterValues(String name) + { + return req.getParameterValues(name); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getPathInfo() + */ + @Override + public String getPathInfo() + { + return req.getPathInfo(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getQueryString() + */ + @Override + public String getQueryString() + { + return req.getQueryString(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getRuntime() + */ + @Override + public Runtime getRuntime() + { + return req.getRuntime(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getServerPath() + */ + @Override + public String getServerPath() + { + return req.getServerPath(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getServiceContextPath() + */ + @Override + public String getServiceContextPath() + { + return req.getServiceContextPath(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getServiceMatch() + */ + @Override + public Match getServiceMatch() + { + return req.getServiceMatch(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getServicePath() + */ + @Override + public String getServicePath() + { + return req.getServicePath(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getURL() + */ + @Override + public String getURL() + { + return req.getURL(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#isGuest() + */ + @Override + public boolean isGuest() + { + return req.isGuest(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#parseContent() + */ + @Override + public Object parseContent() + { + return req.parseContent(); + } +} diff --git a/src/main/java/org/alfresco/repo/web/scripts/BufferedResponse.java b/src/main/java/org/alfresco/repo/web/scripts/BufferedResponse.java index b6520fb35a..6b32bf3411 100644 --- a/src/main/java/org/alfresco/repo/web/scripts/BufferedResponse.java +++ b/src/main/java/org/alfresco/repo/web/scripts/BufferedResponse.java @@ -1,276 +1,274 @@ -/* - * #%L - * Alfresco Remote API - * %% - * Copyright (C) 2005 - 2019 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ -package org.alfresco.repo.web.scripts; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.Writer; - -import org.alfresco.error.AlfrescoRuntimeException; -import org.apache.chemistry.opencmis.commons.server.TempStoreOutputStream; -import org.apache.chemistry.opencmis.server.shared.TempStoreOutputStreamFactory; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.extensions.surf.util.StringBuilderWriter; -import org.springframework.extensions.webscripts.Cache; -import org.springframework.extensions.webscripts.Runtime; -import org.springframework.extensions.webscripts.WebScriptResponse; -import org.springframework.extensions.webscripts.WrappingWebScriptResponse; -import org.springframework.util.FileCopyUtils; - -/** - * Transactional Buffered Response - */ -public class BufferedResponse implements WrappingWebScriptResponse -{ - // Logger - protected static final Log logger = LogFactory.getLog(BufferedResponse.class); - - private TempStoreOutputStreamFactory streamFactory; - private WebScriptResponse res; - private int bufferSize; - private TempStoreOutputStream outputStream = null; - private StringBuilderWriter outputWriter = null; - - - /** - * Construct - * - * @param res WebScriptResponse - * @param bufferSize int - */ - public BufferedResponse(WebScriptResponse res, int bufferSize, TempStoreOutputStreamFactory streamFactory) - { - this.res = res; - this.bufferSize = bufferSize; - this.streamFactory = streamFactory; - } - - /* - * (non-Javadoc) - * @see org.alfresco.web.scripts.WrappingWebScriptResponse#getNext() - */ - public WebScriptResponse getNext() - { - return res; - } - - /* - * (non-Javadoc) - * @see org.alfresco.web.scripts.WebScriptResponse#addHeader(java.lang.String, java.lang.String) - */ - public void addHeader(String name, String value) - { - res.addHeader(name, value); - } - - /* - * (non-Javadoc) - * @see org.alfresco.web.scripts.WebScriptResponse#encodeScriptUrl(java.lang.String) - */ - public String encodeScriptUrl(String url) - { - return res.encodeScriptUrl(url); - } - - /* - * (non-Javadoc) - * @see org.alfresco.web.scripts.WebScriptResponse#getEncodeScriptUrlFunction(java.lang.String) - */ - public String getEncodeScriptUrlFunction(String name) - { - return res.getEncodeScriptUrlFunction(name); - } - - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptResponse#encodeResourceUrl(java.lang.String) - */ - public String encodeResourceUrl(String url) - { - return res.encodeResourceUrl(url); - } - - /* (non-Javadoc) - * @see org.springframework.extensions.webscripts.WebScriptResponse#getEncodeResourceUrlFunction(java.lang.String) - */ - public String getEncodeResourceUrlFunction(String name) - { - return res.getEncodeResourceUrlFunction(name); - } - - /* - * (non-Javadoc) - * @see org.alfresco.web.scripts.WebScriptResponse#getOutputStream() - */ - public OutputStream getOutputStream() throws IOException - { - if (outputStream == null) - { - if (outputWriter != null) - { - throw new AlfrescoRuntimeException("Already buffering output writer"); - } - outputStream = streamFactory.newOutputStream(); - } - return outputStream; - } - - /* - * (non-Javadoc) - * @see org.alfresco.web.scripts.WebScriptResponse#getRuntime() - */ - public Runtime getRuntime() - { - return res.getRuntime(); - } - - /* - * (non-Javadoc) - * @see org.alfresco.web.scripts.WebScriptResponse#getWriter() - */ - public Writer getWriter() throws IOException - { - if (outputWriter == null) - { - if (outputStream != null) - { - throw new AlfrescoRuntimeException("Already buffering output stream"); - } - outputWriter = new StringBuilderWriter(bufferSize); - } - return outputWriter; - } - - /* - * (non-Javadoc) - * @see org.alfresco.web.scripts.WebScriptResponse#reset() - */ - public void reset() - { - if (outputStream != null) - { - outputStream = null; - } - else if (outputWriter != null) - { - outputWriter = null; - } - res.reset(); - } - - /* (non-Javadoc) - * @see org./alfresco.web.scripts.WebScriptResponse#resetjava.lang.String) - */ - public void reset(String preserveHeadersPattern) - { - if (outputStream != null) - { - outputStream = null; - } - else if (outputWriter != null) - { - outputWriter = null; - } - res.reset(preserveHeadersPattern); - } - - /* - * (non-Javadoc) - * @see org.alfresco.web.scripts.WebScriptResponse#setCache(org.alfresco.web.scripts.Cache) - */ - public void setCache(Cache cache) - { - res.setCache(cache); - } - - /* - * (non-Javadoc) - * @see org.alfresco.web.scripts.WebScriptResponse#setContentType(java.lang.String) - */ - public void setContentType(String contentType) - { - res.setContentType(contentType); - } - - /* - * (non-Javadoc) - * @see org.alfresco.web.scripts.WebScriptResponse#setContentEncoding(java.lang.String) - */ - public void setContentEncoding(String contentEncoding) - { - res.setContentEncoding(contentEncoding); - } - - /* - * (non-Javadoc) - * @see org.alfresco.web.scripts.WebScriptResponse#setHeader(java.lang.String, java.lang.String) - */ - public void setHeader(String name, String value) - { - res.setHeader(name, value); - } - - /* - * (non-Javadoc) - * @see org.alfresco.web.scripts.WebScriptResponse#setStatus(int) - */ - public void setStatus(int status) - { - res.setStatus(status); - } - - /** - * Write buffered response to underlying response - */ - public void writeResponse() - { - try - { - if (logger.isDebugEnabled() && outputStream != null) - { - logger.debug("Writing Transactional response: size=" + outputStream.getLength()); - } - - if (outputWriter != null) - { - outputWriter.flush(); - res.getWriter().write(outputWriter.toString()); - } - else if (outputStream != null) - { - if (logger.isDebugEnabled()) - logger.debug("Writing Transactional response: size=" + outputStream.getLength()); - - outputStream.flush(); - FileCopyUtils.copy(outputStream.getInputStream(), res.getOutputStream()); - } - } - catch (IOException e) - { - throw new AlfrescoRuntimeException("Failed to commit buffered response", e); - } - } -} +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.StringBuilderWriter; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Runtime; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.WrappingWebScriptResponse; +import org.springframework.util.FileCopyUtils; + +/** + * Transactional Buffered Response + */ +public class BufferedResponse implements WrappingWebScriptResponse +{ + // Logger + protected static final Log logger = LogFactory.getLog(BufferedResponse.class); + + private TempOutputStreamFactory streamFactory; + private WebScriptResponse res; + private int bufferSize; + private TempOutputStream outputStream = null; + private StringBuilderWriter outputWriter = null; + + + /** + * Construct + * + * @param res WebScriptResponse + * @param bufferSize int + */ + public BufferedResponse(WebScriptResponse res, int bufferSize, TempOutputStreamFactory streamFactory) + { + this.res = res; + this.bufferSize = bufferSize; + this.streamFactory = streamFactory; + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WrappingWebScriptResponse#getNext() + */ + public WebScriptResponse getNext() + { + return res; + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#addHeader(java.lang.String, java.lang.String) + */ + public void addHeader(String name, String value) + { + res.addHeader(name, value); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#encodeScriptUrl(java.lang.String) + */ + public String encodeScriptUrl(String url) + { + return res.encodeScriptUrl(url); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#getEncodeScriptUrlFunction(java.lang.String) + */ + public String getEncodeScriptUrlFunction(String name) + { + return res.getEncodeScriptUrlFunction(name); + } + + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptResponse#encodeResourceUrl(java.lang.String) + */ + public String encodeResourceUrl(String url) + { + return res.encodeResourceUrl(url); + } + + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptResponse#getEncodeResourceUrlFunction(java.lang.String) + */ + public String getEncodeResourceUrlFunction(String name) + { + return res.getEncodeResourceUrlFunction(name); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#getOutputStream() + */ + public OutputStream getOutputStream() throws IOException + { + if (outputStream == null) + { + if (outputWriter != null) + { + throw new AlfrescoRuntimeException("Already buffering output writer"); + } + outputStream = streamFactory.createOutputStream(); + } + return outputStream; + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#getRuntime() + */ + public Runtime getRuntime() + { + return res.getRuntime(); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#getWriter() + */ + public Writer getWriter() throws IOException + { + if (outputWriter == null) + { + if (outputStream != null) + { + throw new AlfrescoRuntimeException("Already buffering output stream"); + } + outputWriter = new StringBuilderWriter(bufferSize); + } + return outputWriter; + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#reset() + */ + public void reset() + { + if (outputStream != null) + { + outputStream = null; + } + else if (outputWriter != null) + { + outputWriter = null; + } + res.reset(); + } + + /* (non-Javadoc) + * @see org./alfresco.web.scripts.WebScriptResponse#resetjava.lang.String) + */ + public void reset(String preserveHeadersPattern) + { + if (outputStream != null) + { + outputStream = null; + } + else if (outputWriter != null) + { + outputWriter = null; + } + res.reset(preserveHeadersPattern); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#setCache(org.alfresco.web.scripts.Cache) + */ + public void setCache(Cache cache) + { + res.setCache(cache); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#setContentType(java.lang.String) + */ + public void setContentType(String contentType) + { + res.setContentType(contentType); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#setContentEncoding(java.lang.String) + */ + public void setContentEncoding(String contentEncoding) + { + res.setContentEncoding(contentEncoding); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#setHeader(java.lang.String, java.lang.String) + */ + public void setHeader(String name, String value) + { + res.setHeader(name, value); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#setStatus(int) + */ + public void setStatus(int status) + { + res.setStatus(status); + } + + /** + * Write buffered response to underlying response + */ + public void writeResponse() + { + try + { + if (logger.isDebugEnabled() && outputStream != null) + { + logger.debug("Writing Transactional response: size=" + outputStream.getLength()); + } + + if (outputWriter != null) + { + outputWriter.flush(); + res.getWriter().write(outputWriter.toString()); + } + else if (outputStream != null) + { + if (logger.isDebugEnabled()) + logger.debug("Writing Transactional response: size=" + outputStream.getLength()); + + outputStream.flush(); + FileCopyUtils.copy(outputStream.getInputStream(), res.getOutputStream()); + } + } + catch (IOException e) + { + throw new AlfrescoRuntimeException("Failed to commit buffered response", e); + } + } +} diff --git a/src/main/java/org/alfresco/repo/web/scripts/RepositoryContainer.java b/src/main/java/org/alfresco/repo/web/scripts/RepositoryContainer.java index e3bb07f362..7686f302c4 100644 --- a/src/main/java/org/alfresco/repo/web/scripts/RepositoryContainer.java +++ b/src/main/java/org/alfresco/repo/web/scripts/RepositoryContainer.java @@ -1,754 +1,755 @@ -/* - * #%L - * Alfresco Remote API - * %% - * Copyright (C) 2005 - 2019 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ -package org.alfresco.repo.web.scripts; - -import java.io.File; -import java.io.IOException; -import java.net.SocketException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.servlet.http.HttpServletResponse; -import javax.transaction.Status; -import javax.transaction.UserTransaction; - -import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.error.ExceptionStackUtil; -import org.alfresco.repo.model.Repository; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; -import org.alfresco.repo.transaction.AlfrescoTransactionSupport; -import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; -import org.alfresco.repo.transaction.RetryingTransactionHelper; -import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; -import org.alfresco.repo.transaction.TooBusyException; -import org.alfresco.repo.web.scripts.bean.LoginPost; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.TemplateService; -import org.alfresco.service.cmr.security.AuthorityService; -import org.alfresco.service.descriptor.DescriptorService; -import org.alfresco.service.transaction.TransactionService; -import org.alfresco.util.TempFileProvider; -import org.apache.chemistry.opencmis.server.shared.TempStoreOutputStreamFactory; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationEvent; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.extensions.webscripts.AbstractRuntimeContainer; -import org.springframework.extensions.webscripts.Authenticator; -import org.springframework.extensions.webscripts.Description; -import org.springframework.extensions.webscripts.Description.RequiredAuthentication; -import org.springframework.extensions.webscripts.Description.RequiredTransaction; -import org.springframework.extensions.webscripts.Description.RequiredTransactionParameters; -import org.springframework.extensions.webscripts.Description.TransactionCapability; -import org.springframework.extensions.webscripts.ServerModel; -import org.springframework.extensions.webscripts.WebScript; -import org.springframework.extensions.webscripts.WebScriptException; -import org.springframework.extensions.webscripts.WebScriptRequest; -import org.springframework.extensions.webscripts.WebScriptResponse; - - -/** - * Repository (server-tier) container for Web Scripts - * - * @author steveglover - * @author davidc - */ -public class RepositoryContainer extends AbstractRuntimeContainer -{ - // Logger - protected static final Log logger = LogFactory.getLog(RepositoryContainer.class); - - /** Component Dependencies */ - private Repository repository; - private RepositoryImageResolver imageResolver; - private TransactionService transactionService; - private RetryingTransactionHelper fallbackTransactionHelper; - private AuthorityService authorityService; - private DescriptorService descriptorService; - - private boolean encryptTempFiles = false; - private String tempDirectoryName = null; - private int memoryThreshold = 4 * 1024 * 1024; // 4mb - private long maxContentSize = (long) 4 * 1024 * 1024 * 1024; // 4gb - private TempStoreOutputStreamFactory streamFactory = null; - private String preserveHeadersPattern = null; - - private Class[] notPublicExceptions = new Class[] {}; - private Class[] publicExceptions = new Class[] {}; - - /* - * Shame init is already used (by TenantRepositoryContainer). - */ - public void setup() - { - File tempDirectory = TempFileProvider.getTempDir(tempDirectoryName); - this.streamFactory = TempStoreOutputStreamFactory.newInstance(tempDirectory, memoryThreshold, maxContentSize, encryptTempFiles); - } - - public void setEncryptTempFiles(Boolean encryptTempFiles) - { - if(encryptTempFiles != null) - { - this.encryptTempFiles = encryptTempFiles.booleanValue(); - } - } - - public void setTempDirectoryName(String tempDirectoryName) - { - this.tempDirectoryName = tempDirectoryName; - } - - public void setMemoryThreshold(Integer memoryThreshold) - { - if(memoryThreshold != null) - { - this.memoryThreshold = memoryThreshold.intValue(); - } - } - - public void setMaxContentSize(Long maxContentSize) - { - if(maxContentSize != null) - { - this.maxContentSize = maxContentSize.longValue(); - } - } - - public void setPreserveHeadersPattern(String preserveHeadersPattern) - { - this.preserveHeadersPattern = preserveHeadersPattern; - } - - /** - * @param repository Repository - */ - public void setRepository(Repository repository) - { - this.repository = repository; - } - - /** - * @param imageResolver RepositoryImageResolver - */ - public void setRepositoryImageResolver(RepositoryImageResolver imageResolver) - { - this.imageResolver = imageResolver; - } - - /** - * @param transactionService TransactionService - */ - public void setTransactionService(TransactionService transactionService) - { - this.transactionService = transactionService; - } - - /** - * @param fallbackTransactionHelper an unlimited transaction helper used to generate error responses - */ - public void setFallbackTransactionHelper(RetryingTransactionHelper fallbackTransactionHelper) - { - this.fallbackTransactionHelper = fallbackTransactionHelper; - } - - /** - * @param descriptorService DescriptorService - */ - public void setDescriptorService(DescriptorService descriptorService) - { - this.descriptorService = descriptorService; - } - - /** - * @param authorityService AuthorityService - */ - public void setAuthorityService(AuthorityService authorityService) - { - this.authorityService = authorityService; - } - - /** - * Exceptions which may contain information that cannot be displayed in UI - * - * @param notPublicExceptions - {@link Class}<?>[] instance which contains list of not public exceptions - */ - public void setNotPublicExceptions(List> notPublicExceptions) - { - this.notPublicExceptions = new Class[] {}; - if((null != notPublicExceptions) && !notPublicExceptions.isEmpty()) - { - this.notPublicExceptions = notPublicExceptions.toArray(this.notPublicExceptions); - } - } - - public Class[] getNotPublicExceptions() - { - return notPublicExceptions; - } - - /** - * Exceptions which may contain information that need to display in UI - * - * @param publicExceptions - {@link Class}<?>[] instance which contains list of public exceptions - */ - public void setPublicExceptions(List> publicExceptions) - { - this.publicExceptions = new Class[] {}; - if((null != publicExceptions) && !publicExceptions.isEmpty()) - { - this.publicExceptions = publicExceptions.toArray(this.publicExceptions); - } - } - - public Class[] getPublicExceptions() - { - return publicExceptions; - } - - /* (non-Javadoc) - * @see org.alfresco.web.scripts.Container#getDescription() - */ - public ServerModel getDescription() - { - return new RepositoryServerModel(descriptorService.getCurrentRepositoryDescriptor(), descriptorService.getServerDescriptor()); - } - - /* (non-Javadoc) - * @see org.alfresco.web.scripts.AbstractRuntimeContainer#getScriptParameters() - */ - public Map getScriptParameters() - { - Map params = new HashMap(); - params.putAll(super.getScriptParameters()); - addRepoParameters(params); - return params; - } - - /* - * (non-Javadoc) - * @see org.alfresco.web.scripts.AbstractRuntimeContainer#getTemplateParameters() - */ - public Map getTemplateParameters() - { - // Ensure we have a transaction - we might be generating the status template after the main transaction failed - return fallbackTransactionHelper.doInTransaction(new RetryingTransactionCallback>() - { - public Map execute() throws Throwable - { - Map params = new HashMap(); - params.putAll(RepositoryContainer.super.getTemplateParameters()); - params.put(TemplateService.KEY_IMAGE_RESOLVER, imageResolver.getImageResolver()); - addRepoParameters(params); - return params; - } - }, true); - } - - /** - * Add Repository specific parameters - * - * @param params Map - */ - private void addRepoParameters(Map params) - { - if (AlfrescoTransactionSupport.getTransactionId() != null && - AuthenticationUtil.getFullAuthentication() != null) - { - NodeRef rootHome = repository.getRootHome(); - if (rootHome != null) - { - params.put("roothome", rootHome); - } - NodeRef companyHome = repository.getCompanyHome(); - if (companyHome != null) - { - params.put("companyhome", companyHome); - } - NodeRef person = repository.getFullyAuthenticatedPerson(); - if (person != null) - { - params.put("person", person); - NodeRef userHome = repository.getUserHome(person); - if (userHome != null) - { - params.put("userhome", userHome); - } - } - } - } - - /* (non-Javadoc) - * @see org.alfresco.web.scripts.RuntimeContainer#executeScript(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse, org.alfresco.web.scripts.Authenticator) - */ - public void executeScript(WebScriptRequest scriptReq, WebScriptResponse scriptRes, final Authenticator auth) - throws IOException - { - try - { - executeScriptInternal(scriptReq, scriptRes, auth); - } - catch (RuntimeException e) - { - Throwable hideCause = ExceptionStackUtil.getCause(e, notPublicExceptions); - Throwable displayCause = ExceptionStackUtil.getCause(e, publicExceptions); - if (displayCause == null && hideCause != null) - { - AlfrescoRuntimeException alf = null; - if (e instanceof AlfrescoRuntimeException) - { - alf = (AlfrescoRuntimeException) e; - } - else - { - // The message will not have a numerical identifier - alf = new AlfrescoRuntimeException("WebScript execution failed", e); - } - String num = alf.getNumericalId(); - logger.error("Server error (" + num + ")", e); - throw new RuntimeException("Server error (" + num + "). Details can be found in the server logs."); - } - else - { - throw e; - } - } - } - - protected void executeScriptInternal(WebScriptRequest scriptReq, WebScriptResponse scriptRes, final Authenticator auth) - throws IOException - { - final WebScript script = scriptReq.getServiceMatch().getWebScript(); - final Description desc = script.getDescription(); - final boolean debug = logger.isDebugEnabled(); - - // Escalate the webscript declared level of authentication to the container required authentication - // eg. must be guest if MT is enabled unless credentials are empty - RequiredAuthentication containerRequiredAuthentication = getRequiredAuthentication(); - final RequiredAuthentication required = (desc.getRequiredAuthentication().compareTo(containerRequiredAuthentication) < 0 && !auth.emptyCredentials() ? containerRequiredAuthentication : desc.getRequiredAuthentication()); - final boolean isGuest = scriptReq.isGuest(); - - if (required == RequiredAuthentication.none) - { - // TODO revisit - cleared here, in-lieu of WebClient clear - //AuthenticationUtil.clearCurrentSecurityContext(); - - transactionedExecuteAs(script, scriptReq, scriptRes); - } - else if ((required == RequiredAuthentication.user || required == RequiredAuthentication.admin) && isGuest) - { - throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + desc.getId() + " requires user authentication; however, a guest has attempted access."); - } - else - { - try - { - AuthenticationUtil.pushAuthentication(); - - // - // Determine if user already authenticated - // - if (debug) - { - String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); - logger.debug("Current authentication: " + (currentUser == null ? "unauthenticated" : "authenticated as " + currentUser)); - logger.debug("Authentication required: " + required); - logger.debug("Guest login requested: " + isGuest); - } - - // - // Apply appropriate authentication to Web Script invocation - // - RetryingTransactionCallback authWork = new RetryingTransactionCallback() - { - public Boolean execute() throws Exception - { - if (auth == null || auth.authenticate(required, isGuest)) - { - // The user will now have been authenticated, based on HTTP Auth, Ticket etc - // Check that the user they authenticated as has appropriate access to the script - - // Check to see if they supplied HTTP Auth or Ticket as guest, on a script that needs more - if (required == RequiredAuthentication.user || required == RequiredAuthentication.admin) - { - String authenticatedUser = AuthenticationUtil.getFullyAuthenticatedUser(); - String runAsUser = AuthenticationUtil.getRunAsUser(); - - if ( (authenticatedUser == null) || - (authenticatedUser.equals(runAsUser) && authorityService.hasGuestAuthority()) || - (!authenticatedUser.equals(runAsUser) && authorityService.isGuestAuthority(authenticatedUser)) ) - { - throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + desc.getId() + " requires user authentication; however, a guest has attempted access."); - } - } - - // Check to see if they're admin or system on an Admin only script - if (required == RequiredAuthentication.admin && !(authorityService.hasAdminAuthority() || AuthenticationUtil.getFullyAuthenticatedUser().equals(AuthenticationUtil.getSystemUserName()))) - { - throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + desc.getId() + " requires admin authentication; however, a non-admin has attempted access."); - } - - if (debug) - { - String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); - logger.debug("Authentication: " + (currentUser == null ? "unauthenticated" : "authenticated as " + currentUser)); - } - - return true; - } - return false; - } - }; - - boolean readOnly = transactionService.isReadOnly(); - boolean requiresNew = !readOnly && AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY; - if (transactionService.getRetryingTransactionHelper().doInTransaction(authWork, readOnly, requiresNew)) - { - // Execute Web Script if authentication passed - // The Web Script has its own txn management with potential runAs() user - transactionedExecuteAs(script, scriptReq, scriptRes); - } - else - { - throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed for Web Script " + desc.getId()); - } - } - finally - { - // - // Reset authentication for current thread - // - AuthenticationUtil.popAuthentication(); - - if (debug) - { - String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); - logger.debug("Authentication reset: " + (currentUser == null ? "unauthenticated" : "authenticated as " + currentUser)); - } - } - } - } - - /** - * Execute script within required level of transaction - * - * @param script WebScript - * @param scriptReq WebScriptRequest - * @param scriptRes WebScriptResponse - * @throws IOException - */ - protected void transactionedExecute(final WebScript script, final WebScriptRequest scriptReq, final WebScriptResponse scriptRes) - throws IOException - { - try - { - final Description description = script.getDescription(); - if (description.getRequiredTransaction() == RequiredTransaction.none) - { - script.execute(scriptReq, scriptRes); - } - else - { - final BufferedRequest bufferedReq; - final BufferedResponse bufferedRes; - RequiredTransactionParameters trxParams = description.getRequiredTransactionParameters(); - if (trxParams.getCapability() == TransactionCapability.readwrite) - { - if (trxParams.getBufferSize() > 0) - { - if (logger.isDebugEnabled()) - logger.debug("Creating Transactional Response for ReadWrite transaction; buffersize=" + trxParams.getBufferSize()); - - // create buffered request and response that allow transaction retrying - bufferedReq = new BufferedRequest(scriptReq, streamFactory); - bufferedRes = new BufferedResponse(scriptRes, trxParams.getBufferSize(), streamFactory); - } - else - { - if (logger.isDebugEnabled()) - logger.debug("Transactional Response bypassed for ReadWrite - buffersize=0"); - bufferedReq = null; - bufferedRes = null; - } - } - else - { - bufferedReq = null; - bufferedRes = null; - } - - // encapsulate script within transaction - RetryingTransactionCallback work = new RetryingTransactionCallback() - { - public Object execute() throws Exception - { - try - { - if (logger.isDebugEnabled()) - logger.debug("Begin retry transaction block: " + description.getRequiredTransaction() + "," - + description.getRequiredTransactionParameters().getCapability()); - - if (bufferedRes == null) - { - script.execute(scriptReq, scriptRes); - } - else - { - // Reset the request and response in case of a transaction retry - bufferedReq.reset(); - // REPO-4388 don't reset specified headers - bufferedRes.reset(preserveHeadersPattern); - script.execute(bufferedReq, bufferedRes); - } - } - catch(Exception e) - { - if (logger.isDebugEnabled()) - { - logger.debug("Transaction exception: " + description.getRequiredTransaction() + ": " + e.getMessage()); - // Note: user transaction shouldn't be null, but just in case inside this exception handler - UserTransaction userTrx = RetryingTransactionHelper.getActiveUserTransaction(); - if (userTrx != null) - { - logger.debug("Transaction status: " + userTrx.getStatus()); - } - } - - UserTransaction userTrx = RetryingTransactionHelper.getActiveUserTransaction(); - if (userTrx != null) - { - if (userTrx.getStatus() != Status.STATUS_MARKED_ROLLBACK) - { - if (logger.isDebugEnabled()) - logger.debug("Marking web script transaction for rollback"); - try - { - userTrx.setRollbackOnly(); - } - catch(Throwable re) - { - if (logger.isDebugEnabled()) - logger.debug("Caught and ignoring exception during marking for rollback: " + re.getMessage()); - } - } - } - - // re-throw original exception for retry - throw e; - } - finally - { - if (logger.isDebugEnabled()) - logger.debug("End retry transaction block: " + description.getRequiredTransaction() + "," - + description.getRequiredTransactionParameters().getCapability()); - } - - return null; - } - }; - - boolean readonly = description.getRequiredTransactionParameters().getCapability() == TransactionCapability.readonly; - boolean requiresNew = description.getRequiredTransaction() == RequiredTransaction.requiresnew; - - // log a warning if we detect a GET webscript being run in a readwrite transaction, GET calls should - // NOT have any side effects so this scenario as a warning sign something maybe amiss, see ALF-10179. - if (logger.isDebugEnabled() && !readonly && "GET".equalsIgnoreCase(description.getMethod())) - { - logger.debug("Webscript with URL '" + scriptReq.getURL() + - "' is a GET request but it's descriptor has declared a readwrite transaction is required"); - } - - try - { - RetryingTransactionHelper transactionHelper = transactionService.getRetryingTransactionHelper(); - if(script instanceof LoginPost) - { - //login script requires read-write transaction because of authorization intercepter - transactionHelper.setForceWritable(true); - } - transactionHelper.doInTransaction(work, readonly, requiresNew); - } - catch (TooBusyException e) - { - // Map TooBusyException to a 503 status code - throw new WebScriptException(HttpServletResponse.SC_SERVICE_UNAVAILABLE, e.getMessage(), e); - } - finally - { - // Get rid of any temporary files - if (bufferedReq != null) - { - bufferedReq.close(); - } - } - - // Ensure a response is always flushed after successful execution - if (bufferedRes != null) - { - bufferedRes.writeResponse(); - } - - } - } - catch (IOException ioe) - { - Throwable socketException = ExceptionStackUtil.getCause(ioe, SocketException.class); - Class clientAbortException = null; - try - { - clientAbortException = Class.forName("org.apache.catalina.connector.ClientAbortException"); - } - catch (ClassNotFoundException e) - { - // do nothing - } - // Note: if you need to look for more exceptions in the stack, then create a static array and pass it in - if ((socketException != null && socketException.getMessage().contains("Broken pipe")) || (clientAbortException != null && ExceptionStackUtil.getCause(ioe, clientAbortException) != null)) - { - if (logger.isDebugEnabled()) - { - logger.warn("Client has cut off communication", ioe); - } - else - { - logger.info("Client has cut off communication"); - } - } - else - { - throw ioe; - } - } - } - - /** - * Execute script within required level of transaction as required effective user. - * - * @param script WebScript - * @param scriptReq WebScriptRequest - * @param scriptRes WebScriptResponse - * @throws IOException - */ - private void transactionedExecuteAs(final WebScript script, final WebScriptRequest scriptReq, - final WebScriptResponse scriptRes) throws IOException - { - String runAs = script.getDescription().getRunAs(); - if (runAs == null) - { - transactionedExecute(script, scriptReq, scriptRes); - } - else - { - RunAsWork work = new RunAsWork() - { - public Object doWork() throws Exception - { - transactionedExecute(script, scriptReq, scriptRes); - return null; - } - }; - AuthenticationUtil.runAs(work, runAs); - } - } - - /* (non-Javadoc) - * @see org.alfresco.web.scripts.AbstractRuntimeContainer#onApplicationEvent(org.springframework.context.ApplicationEvent) - */ - @Override - public void onApplicationEvent(ApplicationEvent event) - { - if (event instanceof ContextRefreshedEvent) - { - ContextRefreshedEvent refreshEvent = (ContextRefreshedEvent)event; - ApplicationContext refreshContext = refreshEvent.getApplicationContext(); - if (refreshContext != null && refreshContext.equals(applicationContext)) - { - RunAsWork work = new RunAsWork() - { - public Object doWork() throws Exception - { - reset(); - return null; - } - }; - AuthenticationUtil.runAs(work, AuthenticationUtil.getSystemUserName()); - } - } - } - - /* (non-Javadoc) - * @see org.alfresco.web.scripts.AbstractRuntimeContainer#getRequiredAuthentication() - */ - @Override - public RequiredAuthentication getRequiredAuthentication() - { - if (AuthenticationUtil.isMtEnabled()) - { - return RequiredAuthentication.guest; // user or guest (ie. at least guest) - } - - return RequiredAuthentication.none; - } - - /* (non-Javadoc) - * @see org.alfresco.web.scripts.RuntimeContainer#authenticate(org.alfresco.web.scripts.Authenticator, org.alfresco.web.scripts.Description.RequiredAuthentication) - */ - @Override - public boolean authenticate(Authenticator auth, RequiredAuthentication required) - { - if (auth != null) - { - AuthenticationUtil.clearCurrentSecurityContext(); - - return auth.authenticate(required, false); - } - - return false; - } - - /* (non-Javadoc) - * @see org.alfresco.web.scripts.AbstractRuntimeContainer#reset() - */ - @Override - public void reset() - { - transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() - { - public Object execute() throws Exception - { - internalReset(); - return null; - } - }, true, false); - } - - private void internalReset() - { - super.reset(); - } -} +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.io.File; +import java.io.IOException; +import java.net.SocketException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.error.ExceptionStackUtil; +import org.alfresco.repo.model.Repository; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.repo.transaction.TooBusyException; +import org.alfresco.repo.web.scripts.bean.LoginPost; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.TemplateService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.descriptor.DescriptorService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.extensions.webscripts.AbstractRuntimeContainer; +import org.springframework.extensions.webscripts.Authenticator; +import org.springframework.extensions.webscripts.Description; +import org.springframework.extensions.webscripts.Description.RequiredAuthentication; +import org.springframework.extensions.webscripts.Description.RequiredTransaction; +import org.springframework.extensions.webscripts.Description.RequiredTransactionParameters; +import org.springframework.extensions.webscripts.Description.TransactionCapability; +import org.springframework.extensions.webscripts.ServerModel; +import org.springframework.extensions.webscripts.WebScript; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + + +/** + * Repository (server-tier) container for Web Scripts + * + * @author steveglover + * @author davidc + */ +public class RepositoryContainer extends AbstractRuntimeContainer +{ + // Logger + protected static final Log logger = LogFactory.getLog(RepositoryContainer.class); + + /** Component Dependencies */ + private Repository repository; + private RepositoryImageResolver imageResolver; + private TransactionService transactionService; + private RetryingTransactionHelper fallbackTransactionHelper; + private AuthorityService authorityService; + private DescriptorService descriptorService; + + private boolean encryptTempFiles = false; + private String tempDirectoryName = null; + private int memoryThreshold = 4 * 1024 * 1024; // 4mb + private long maxContentSize = (long) 4 * 1024 * 1024 * 1024; // 4gb + private TempOutputStreamFactory streamFactory = null; + private TempOutputStreamFactory responseStreamFactory = null; + private String preserveHeadersPattern = null; + + private Class[] notPublicExceptions = new Class[] {}; + private Class[] publicExceptions = new Class[] {}; + + /* + * Shame init is already used (by TenantRepositoryContainer). + */ + public void setup() + { + File tempDirectory = TempFileProvider.getTempDir(tempDirectoryName); + this.streamFactory = new TempOutputStreamFactory(tempDirectory, memoryThreshold, maxContentSize, encryptTempFiles, false); + this.responseStreamFactory = new TempOutputStreamFactory(tempDirectory, memoryThreshold, maxContentSize, encryptTempFiles, true); + } + + public void setEncryptTempFiles(Boolean encryptTempFiles) + { + if(encryptTempFiles != null) + { + this.encryptTempFiles = encryptTempFiles.booleanValue(); + } + } + + public void setTempDirectoryName(String tempDirectoryName) + { + this.tempDirectoryName = tempDirectoryName; + } + + public void setMemoryThreshold(Integer memoryThreshold) + { + if(memoryThreshold != null) + { + this.memoryThreshold = memoryThreshold.intValue(); + } + } + + public void setMaxContentSize(Long maxContentSize) + { + if(maxContentSize != null) + { + this.maxContentSize = maxContentSize.longValue(); + } + } + + public void setPreserveHeadersPattern(String preserveHeadersPattern) + { + this.preserveHeadersPattern = preserveHeadersPattern; + } + + /** + * @param repository Repository + */ + public void setRepository(Repository repository) + { + this.repository = repository; + } + + /** + * @param imageResolver RepositoryImageResolver + */ + public void setRepositoryImageResolver(RepositoryImageResolver imageResolver) + { + this.imageResolver = imageResolver; + } + + /** + * @param transactionService TransactionService + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * @param fallbackTransactionHelper an unlimited transaction helper used to generate error responses + */ + public void setFallbackTransactionHelper(RetryingTransactionHelper fallbackTransactionHelper) + { + this.fallbackTransactionHelper = fallbackTransactionHelper; + } + + /** + * @param descriptorService DescriptorService + */ + public void setDescriptorService(DescriptorService descriptorService) + { + this.descriptorService = descriptorService; + } + + /** + * @param authorityService AuthorityService + */ + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + /** + * Exceptions which may contain information that cannot be displayed in UI + * + * @param notPublicExceptions - {@link Class}<?>[] instance which contains list of not public exceptions + */ + public void setNotPublicExceptions(List> notPublicExceptions) + { + this.notPublicExceptions = new Class[] {}; + if((null != notPublicExceptions) && !notPublicExceptions.isEmpty()) + { + this.notPublicExceptions = notPublicExceptions.toArray(this.notPublicExceptions); + } + } + + public Class[] getNotPublicExceptions() + { + return notPublicExceptions; + } + + /** + * Exceptions which may contain information that need to display in UI + * + * @param publicExceptions - {@link Class}<?>[] instance which contains list of public exceptions + */ + public void setPublicExceptions(List> publicExceptions) + { + this.publicExceptions = new Class[] {}; + if((null != publicExceptions) && !publicExceptions.isEmpty()) + { + this.publicExceptions = publicExceptions.toArray(this.publicExceptions); + } + } + + public Class[] getPublicExceptions() + { + return publicExceptions; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Container#getDescription() + */ + public ServerModel getDescription() + { + return new RepositoryServerModel(descriptorService.getCurrentRepositoryDescriptor(), descriptorService.getServerDescriptor()); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.AbstractRuntimeContainer#getScriptParameters() + */ + public Map getScriptParameters() + { + Map params = new HashMap(); + params.putAll(super.getScriptParameters()); + addRepoParameters(params); + return params; + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.AbstractRuntimeContainer#getTemplateParameters() + */ + public Map getTemplateParameters() + { + // Ensure we have a transaction - we might be generating the status template after the main transaction failed + return fallbackTransactionHelper.doInTransaction(new RetryingTransactionCallback>() + { + public Map execute() throws Throwable + { + Map params = new HashMap(); + params.putAll(RepositoryContainer.super.getTemplateParameters()); + params.put(TemplateService.KEY_IMAGE_RESOLVER, imageResolver.getImageResolver()); + addRepoParameters(params); + return params; + } + }, true); + } + + /** + * Add Repository specific parameters + * + * @param params Map + */ + private void addRepoParameters(Map params) + { + if (AlfrescoTransactionSupport.getTransactionId() != null && + AuthenticationUtil.getFullAuthentication() != null) + { + NodeRef rootHome = repository.getRootHome(); + if (rootHome != null) + { + params.put("roothome", rootHome); + } + NodeRef companyHome = repository.getCompanyHome(); + if (companyHome != null) + { + params.put("companyhome", companyHome); + } + NodeRef person = repository.getFullyAuthenticatedPerson(); + if (person != null) + { + params.put("person", person); + NodeRef userHome = repository.getUserHome(person); + if (userHome != null) + { + params.put("userhome", userHome); + } + } + } + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.RuntimeContainer#executeScript(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse, org.alfresco.web.scripts.Authenticator) + */ + public void executeScript(WebScriptRequest scriptReq, WebScriptResponse scriptRes, final Authenticator auth) + throws IOException + { + try + { + executeScriptInternal(scriptReq, scriptRes, auth); + } + catch (RuntimeException e) + { + Throwable hideCause = ExceptionStackUtil.getCause(e, notPublicExceptions); + Throwable displayCause = ExceptionStackUtil.getCause(e, publicExceptions); + if (displayCause == null && hideCause != null) + { + AlfrescoRuntimeException alf = null; + if (e instanceof AlfrescoRuntimeException) + { + alf = (AlfrescoRuntimeException) e; + } + else + { + // The message will not have a numerical identifier + alf = new AlfrescoRuntimeException("WebScript execution failed", e); + } + String num = alf.getNumericalId(); + logger.error("Server error (" + num + ")", e); + throw new RuntimeException("Server error (" + num + "). Details can be found in the server logs."); + } + else + { + throw e; + } + } + } + + protected void executeScriptInternal(WebScriptRequest scriptReq, WebScriptResponse scriptRes, final Authenticator auth) + throws IOException + { + final WebScript script = scriptReq.getServiceMatch().getWebScript(); + final Description desc = script.getDescription(); + final boolean debug = logger.isDebugEnabled(); + + // Escalate the webscript declared level of authentication to the container required authentication + // eg. must be guest if MT is enabled unless credentials are empty + RequiredAuthentication containerRequiredAuthentication = getRequiredAuthentication(); + final RequiredAuthentication required = (desc.getRequiredAuthentication().compareTo(containerRequiredAuthentication) < 0 && !auth.emptyCredentials() ? containerRequiredAuthentication : desc.getRequiredAuthentication()); + final boolean isGuest = scriptReq.isGuest(); + + if (required == RequiredAuthentication.none) + { + // TODO revisit - cleared here, in-lieu of WebClient clear + //AuthenticationUtil.clearCurrentSecurityContext(); + + transactionedExecuteAs(script, scriptReq, scriptRes); + } + else if ((required == RequiredAuthentication.user || required == RequiredAuthentication.admin) && isGuest) + { + throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + desc.getId() + " requires user authentication; however, a guest has attempted access."); + } + else + { + try + { + AuthenticationUtil.pushAuthentication(); + + // + // Determine if user already authenticated + // + if (debug) + { + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + logger.debug("Current authentication: " + (currentUser == null ? "unauthenticated" : "authenticated as " + currentUser)); + logger.debug("Authentication required: " + required); + logger.debug("Guest login requested: " + isGuest); + } + + // + // Apply appropriate authentication to Web Script invocation + // + RetryingTransactionCallback authWork = new RetryingTransactionCallback() + { + public Boolean execute() throws Exception + { + if (auth == null || auth.authenticate(required, isGuest)) + { + // The user will now have been authenticated, based on HTTP Auth, Ticket etc + // Check that the user they authenticated as has appropriate access to the script + + // Check to see if they supplied HTTP Auth or Ticket as guest, on a script that needs more + if (required == RequiredAuthentication.user || required == RequiredAuthentication.admin) + { + String authenticatedUser = AuthenticationUtil.getFullyAuthenticatedUser(); + String runAsUser = AuthenticationUtil.getRunAsUser(); + + if ( (authenticatedUser == null) || + (authenticatedUser.equals(runAsUser) && authorityService.hasGuestAuthority()) || + (!authenticatedUser.equals(runAsUser) && authorityService.isGuestAuthority(authenticatedUser)) ) + { + throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + desc.getId() + " requires user authentication; however, a guest has attempted access."); + } + } + + // Check to see if they're admin or system on an Admin only script + if (required == RequiredAuthentication.admin && !(authorityService.hasAdminAuthority() || AuthenticationUtil.getFullyAuthenticatedUser().equals(AuthenticationUtil.getSystemUserName()))) + { + throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + desc.getId() + " requires admin authentication; however, a non-admin has attempted access."); + } + + if (debug) + { + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + logger.debug("Authentication: " + (currentUser == null ? "unauthenticated" : "authenticated as " + currentUser)); + } + + return true; + } + return false; + } + }; + + boolean readOnly = transactionService.isReadOnly(); + boolean requiresNew = !readOnly && AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY; + if (transactionService.getRetryingTransactionHelper().doInTransaction(authWork, readOnly, requiresNew)) + { + // Execute Web Script if authentication passed + // The Web Script has its own txn management with potential runAs() user + transactionedExecuteAs(script, scriptReq, scriptRes); + } + else + { + throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed for Web Script " + desc.getId()); + } + } + finally + { + // + // Reset authentication for current thread + // + AuthenticationUtil.popAuthentication(); + + if (debug) + { + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + logger.debug("Authentication reset: " + (currentUser == null ? "unauthenticated" : "authenticated as " + currentUser)); + } + } + } + } + + /** + * Execute script within required level of transaction + * + * @param script WebScript + * @param scriptReq WebScriptRequest + * @param scriptRes WebScriptResponse + * @throws IOException + */ + protected void transactionedExecute(final WebScript script, final WebScriptRequest scriptReq, final WebScriptResponse scriptRes) + throws IOException + { + try + { + final Description description = script.getDescription(); + if (description.getRequiredTransaction() == RequiredTransaction.none) + { + script.execute(scriptReq, scriptRes); + } + else + { + final BufferedRequest bufferedReq; + final BufferedResponse bufferedRes; + RequiredTransactionParameters trxParams = description.getRequiredTransactionParameters(); + if (trxParams.getCapability() == TransactionCapability.readwrite) + { + if (trxParams.getBufferSize() > 0) + { + if (logger.isDebugEnabled()) + logger.debug("Creating Transactional Response for ReadWrite transaction; buffersize=" + trxParams.getBufferSize()); + + // create buffered request and response that allow transaction retrying + bufferedReq = new BufferedRequest(scriptReq, streamFactory); + bufferedRes = new BufferedResponse(scriptRes, trxParams.getBufferSize(), responseStreamFactory); + } + else + { + if (logger.isDebugEnabled()) + logger.debug("Transactional Response bypassed for ReadWrite - buffersize=0"); + bufferedReq = null; + bufferedRes = null; + } + } + else + { + bufferedReq = null; + bufferedRes = null; + } + + // encapsulate script within transaction + RetryingTransactionCallback work = new RetryingTransactionCallback() + { + public Object execute() throws Exception + { + try + { + if (logger.isDebugEnabled()) + logger.debug("Begin retry transaction block: " + description.getRequiredTransaction() + "," + + description.getRequiredTransactionParameters().getCapability()); + + if (bufferedRes == null) + { + script.execute(scriptReq, scriptRes); + } + else + { + // Reset the request and response in case of a transaction retry + bufferedReq.reset(); + // REPO-4388 don't reset specified headers + bufferedRes.reset(preserveHeadersPattern); + script.execute(bufferedReq, bufferedRes); + } + } + catch(Exception e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Transaction exception: " + description.getRequiredTransaction() + ": " + e.getMessage()); + // Note: user transaction shouldn't be null, but just in case inside this exception handler + UserTransaction userTrx = RetryingTransactionHelper.getActiveUserTransaction(); + if (userTrx != null) + { + logger.debug("Transaction status: " + userTrx.getStatus()); + } + } + + UserTransaction userTrx = RetryingTransactionHelper.getActiveUserTransaction(); + if (userTrx != null) + { + if (userTrx.getStatus() != Status.STATUS_MARKED_ROLLBACK) + { + if (logger.isDebugEnabled()) + logger.debug("Marking web script transaction for rollback"); + try + { + userTrx.setRollbackOnly(); + } + catch(Throwable re) + { + if (logger.isDebugEnabled()) + logger.debug("Caught and ignoring exception during marking for rollback: " + re.getMessage()); + } + } + } + + // re-throw original exception for retry + throw e; + } + finally + { + if (logger.isDebugEnabled()) + logger.debug("End retry transaction block: " + description.getRequiredTransaction() + "," + + description.getRequiredTransactionParameters().getCapability()); + } + + return null; + } + }; + + boolean readonly = description.getRequiredTransactionParameters().getCapability() == TransactionCapability.readonly; + boolean requiresNew = description.getRequiredTransaction() == RequiredTransaction.requiresnew; + + // log a warning if we detect a GET webscript being run in a readwrite transaction, GET calls should + // NOT have any side effects so this scenario as a warning sign something maybe amiss, see ALF-10179. + if (logger.isDebugEnabled() && !readonly && "GET".equalsIgnoreCase(description.getMethod())) + { + logger.debug("Webscript with URL '" + scriptReq.getURL() + + "' is a GET request but it's descriptor has declared a readwrite transaction is required"); + } + + try + { + RetryingTransactionHelper transactionHelper = transactionService.getRetryingTransactionHelper(); + if(script instanceof LoginPost) + { + //login script requires read-write transaction because of authorization intercepter + transactionHelper.setForceWritable(true); + } + transactionHelper.doInTransaction(work, readonly, requiresNew); + } + catch (TooBusyException e) + { + // Map TooBusyException to a 503 status code + throw new WebScriptException(HttpServletResponse.SC_SERVICE_UNAVAILABLE, e.getMessage(), e); + } + finally + { + // Get rid of any temporary files + if (bufferedReq != null) + { + bufferedReq.close(); + } + } + + // Ensure a response is always flushed after successful execution + if (bufferedRes != null) + { + bufferedRes.writeResponse(); + } + + } + } + catch (IOException ioe) + { + Throwable socketException = ExceptionStackUtil.getCause(ioe, SocketException.class); + Class clientAbortException = null; + try + { + clientAbortException = Class.forName("org.apache.catalina.connector.ClientAbortException"); + } + catch (ClassNotFoundException e) + { + // do nothing + } + // Note: if you need to look for more exceptions in the stack, then create a static array and pass it in + if ((socketException != null && socketException.getMessage().contains("Broken pipe")) || (clientAbortException != null && ExceptionStackUtil.getCause(ioe, clientAbortException) != null)) + { + if (logger.isDebugEnabled()) + { + logger.warn("Client has cut off communication", ioe); + } + else + { + logger.info("Client has cut off communication"); + } + } + else + { + throw ioe; + } + } + } + + /** + * Execute script within required level of transaction as required effective user. + * + * @param script WebScript + * @param scriptReq WebScriptRequest + * @param scriptRes WebScriptResponse + * @throws IOException + */ + private void transactionedExecuteAs(final WebScript script, final WebScriptRequest scriptReq, + final WebScriptResponse scriptRes) throws IOException + { + String runAs = script.getDescription().getRunAs(); + if (runAs == null) + { + transactionedExecute(script, scriptReq, scriptRes); + } + else + { + RunAsWork work = new RunAsWork() + { + public Object doWork() throws Exception + { + transactionedExecute(script, scriptReq, scriptRes); + return null; + } + }; + AuthenticationUtil.runAs(work, runAs); + } + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.AbstractRuntimeContainer#onApplicationEvent(org.springframework.context.ApplicationEvent) + */ + @Override + public void onApplicationEvent(ApplicationEvent event) + { + if (event instanceof ContextRefreshedEvent) + { + ContextRefreshedEvent refreshEvent = (ContextRefreshedEvent)event; + ApplicationContext refreshContext = refreshEvent.getApplicationContext(); + if (refreshContext != null && refreshContext.equals(applicationContext)) + { + RunAsWork work = new RunAsWork() + { + public Object doWork() throws Exception + { + reset(); + return null; + } + }; + AuthenticationUtil.runAs(work, AuthenticationUtil.getSystemUserName()); + } + } + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.AbstractRuntimeContainer#getRequiredAuthentication() + */ + @Override + public RequiredAuthentication getRequiredAuthentication() + { + if (AuthenticationUtil.isMtEnabled()) + { + return RequiredAuthentication.guest; // user or guest (ie. at least guest) + } + + return RequiredAuthentication.none; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.RuntimeContainer#authenticate(org.alfresco.web.scripts.Authenticator, org.alfresco.web.scripts.Description.RequiredAuthentication) + */ + @Override + public boolean authenticate(Authenticator auth, RequiredAuthentication required) + { + if (auth != null) + { + AuthenticationUtil.clearCurrentSecurityContext(); + + return auth.authenticate(required, false); + } + + return false; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.AbstractRuntimeContainer#reset() + */ + @Override + public void reset() + { + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + public Object execute() throws Exception + { + internalReset(); + return null; + } + }, true, false); + } + + private void internalReset() + { + super.reset(); + } +} diff --git a/src/main/java/org/alfresco/repo/web/scripts/TempOutputStream.java b/src/main/java/org/alfresco/repo/web/scripts/TempOutputStream.java new file mode 100644 index 0000000000..e9f6df704e --- /dev/null +++ b/src/main/java/org/alfresco/repo/web/scripts/TempOutputStream.java @@ -0,0 +1,383 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.Key; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.KeyGenerator; +import javax.crypto.spec.IvParameterSpec; + +import org.alfresco.repo.content.ContentLimitViolationException; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * An output stream implementation that keeps the data in memory if is less then + * the specified memoryThreshold otherwise it writes it to a temp file. + *

+ * + * Close the stream before any call to + * {@link TempOutputStream}.getInputStream(). + *

+ * + * If deleteTempFileOnClose is false then use proper try-finally patterns + * to ensure that the temp file is destroyed after it is no longer needed. + * + *

+ *   try
+ *   {
+ *      StreamUtils.copy(new BufferedInputStream(new FileInputStream(file)), tempOutputStream);
+ *      tempOutputStream.close();
+ *   }
+ *   finally
+ *   {
+ *       tempOutputStream.destroy();
+ *   }
+ *   
+ * 
+ */ +public class TempOutputStream extends OutputStream +{ + private static final Log logger = LogFactory.getLog(TempOutputStream.class); + + private static final int DEFAULT_MEMORY_THRESHOLD = 4 * 1024 * 1024; // 4mb + private static final String ALGORITHM = "AES"; + private static final String MODE = "CTR"; + private static final String PADDING = "PKCS5Padding"; + private static final String TRANSFORMATION = ALGORITHM + '/' + MODE + '/' + PADDING; + private static final int KEY_SIZE = 128; + public static final String TEMP_FILE_PREFIX = "tempStreamFile-"; + + private final File tempDir; + private final int memoryThreshold; + private final long maxContentSize; + private boolean encrypt; + private boolean deleteTempFileOnClose; + + private long length = 0; + private OutputStream outputStream; + private File tempFile; + private TempByteArrayOutputStream tempStream; + + private Key symKey; + private byte[] iv; + + /** + * Creates a TempOutputStream. + * + * @param tempDir + * the temporary directory, i.e. isDir == true, that + * will be used as * parent directory for creating temp file backed + * streams + * @param memoryThreshold + * the memory threshold in B + * @param maxContentSize + * the max content size in B + * @param encrypt + * true if temp files should be encrypted + * @param deleteTempFileOnClose + * true if temp files should be deleted on output stream close + * (useful if we need to cache the content for further reads). If + * this is false then we need to make sure we call + * {@link TempOutputStream}.destroy to clean up properly. + */ + public TempOutputStream(File tempDir, int memoryThreshold, long maxContentSize, boolean encrypt, boolean deleteTempFileOnClose) + { + this.tempDir = tempDir; + this.memoryThreshold = (memoryThreshold < 0) ? DEFAULT_MEMORY_THRESHOLD : memoryThreshold; + this.maxContentSize = maxContentSize; + this.encrypt = encrypt; + this.deleteTempFileOnClose = deleteTempFileOnClose; + + this.tempStream = new TempByteArrayOutputStream(); + this.outputStream = this.tempStream; + } + + /** + * Returns the data as an InputStream + */ + public InputStream getInputStream() throws IOException + { + if (tempFile != null) + { + if (encrypt) + { + final Cipher cipher; + try + { + cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, symKey, new IvParameterSpec(iv)); + } + catch (Exception e) + { + destroy(); + + if (logger.isErrorEnabled()) + { + logger.error("Cannot initialize decryption cipher", e); + } + + throw new IOException("Cannot initialize decryption cipher", e); + } + + return new BufferedInputStream(new CipherInputStream(new FileInputStream(tempFile), cipher)); + } + return new BufferedInputStream(new FileInputStream(tempFile)); + } + else + { + return new ByteArrayInputStream(tempStream.getBuffer(), 0, tempStream.getCount()); + } + } + + @Override + public void write(int b) throws IOException + { + update(1); + outputStream.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException + { + update(len); + outputStream.write(b, off, len); + } + + @Override + public void flush() throws IOException + { + outputStream.flush(); + } + + @Override + public void close() throws IOException + { + close(deleteTempFileOnClose); + } + + /** + * Closes the stream and removes the backing file (if present). + *

+ * + * If deleteTempFileOnClose is false then use proper try-finally patterns + * to ensure that the temp file is destroyed after it is no longer needed. + * + *

+     *   try
+     *   {
+     *      StreamUtils.copy(new BufferedInputStream(new FileInputStream(file)), tempOutputStream);
+     *      tempOutputStream.close();
+     *   }
+     *   finally
+     *   {
+     *       tempOutputStream.destroy();
+     *   }
+     *   
+     * 
+ */ + public void destroy() throws IOException + { + close(true); + } + + public long getLength() + { + return length; + } + + private void closeOutputStream() + { + if (outputStream != null) + { + try + { + outputStream.flush(); + } + catch (IOException e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Flushing the output stream failed", e); + } + } + + try + { + outputStream.close(); + } + catch (IOException e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Closing the output stream failed", e); + } + } + } + } + + private void deleteTempFile() + { + if (tempFile != null) + { + try + { + boolean isDeleted = tempFile.delete(); + if (!isDeleted) + { + if (logger.isDebugEnabled()) + { + logger.debug("Temp file could not be deleted: " + tempFile.getAbsolutePath()); + } + } + else + { + if (logger.isDebugEnabled()) + { + logger.debug("Deleted temp file: " + tempFile.getAbsolutePath()); + } + } + } + finally + { + tempFile = null; + } + } + } + + private void close(boolean deleteTempFileOnClose) + { + closeOutputStream(); + + if (deleteTempFileOnClose) + { + deleteTempFile(); + } + } + + private BufferedOutputStream createOutputStream(File file) throws IOException + { + BufferedOutputStream fileOutputStream; + if (encrypt) + { + try + { + // Generate a symmetric key + final KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM); + keyGen.init(KEY_SIZE); + symKey = keyGen.generateKey(); + + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, symKey); + + iv = cipher.getIV(); + + fileOutputStream = new BufferedOutputStream(new CipherOutputStream(new FileOutputStream(file), cipher)); + } + catch (Exception e) + { + if (logger.isErrorEnabled()) + { + logger.error("Cannot initialize encryption cipher", e); + } + + throw new IOException("Cannot initialize encryption cipher", e); + } + } + else + { + fileOutputStream = new BufferedOutputStream(new FileOutputStream(file)); + } + + return fileOutputStream; + } + + private void update(int len) throws IOException + { + if (maxContentSize > -1 && length + len > maxContentSize) + { + destroy(); + throw new ContentLimitViolationException("Content size violation, limit = " + maxContentSize); + } + + if (tempFile == null && (tempStream.getCount() + len) > memoryThreshold) + { + File file = TempFileProvider.createTempFile(TEMP_FILE_PREFIX, ".bin", tempDir); + + BufferedOutputStream fileOutputStream = createOutputStream(file); + fileOutputStream.write(this.tempStream.getBuffer(), 0, this.tempStream.getCount()); + fileOutputStream.flush(); + + try + { + tempStream.close(); + } + catch (IOException e) + { + // Ignore exception + } + tempStream = null; + + tempFile = file; + outputStream = fileOutputStream; + } + + length += len; + } + + private static class TempByteArrayOutputStream extends ByteArrayOutputStream + { + /** + * @return The internal buffer where data is stored + */ + public byte[] getBuffer() + { + return buf; + } + + /** + * @return The number of valid bytes in the buffer. + */ + public int getCount() + { + return count; + } + } +} diff --git a/src/main/java/org/alfresco/repo/web/scripts/TempOutputStreamFactory.java b/src/main/java/org/alfresco/repo/web/scripts/TempOutputStreamFactory.java new file mode 100644 index 0000000000..2300bb8b03 --- /dev/null +++ b/src/main/java/org/alfresco/repo/web/scripts/TempOutputStreamFactory.java @@ -0,0 +1,105 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.io.File; + +/** + * Factory for {@link TempOutputStream} + */ +public class TempOutputStreamFactory +{ + /** + * A temporary directory, i.e. isDir == true, that will be used as + * parent directory for creating temp file backed streams. + */ + private final File tempDir; + private int memoryThreshold; + private long maxContentSize; + private boolean encrypt; + private boolean deleteTempFileOnClose; + + /** + * Creates a {@link TempOutputStream} factory. + * + * @param tempDir + * the temporary directory, i.e. isDir == true, that + * will be used as * parent directory for creating temp file backed + * streams + * @param memoryThreshold + * the memory threshold in B + * @param maxContentSize + * the max content size in B + * @param encrypt + * true if temp files should be encrypted + * @param deleteTempFileOnClose + * true if temp files should be deleted on output stream close + * (useful if we need to cache the content for further reads). If + * this is false then we need to make sure we call + * {@link TempOutputStream}.destroy to clean up properly. + */ + public TempOutputStreamFactory(File tempDir, int memoryThreshold, long maxContentSize, boolean encrypt, boolean deleteTempFileOnClose) + { + this.tempDir = tempDir; + this.memoryThreshold = memoryThreshold; + this.maxContentSize = maxContentSize; + this.encrypt = encrypt; + this.deleteTempFileOnClose = deleteTempFileOnClose; + } + + /** + * Creates a new {@link TempOutputStream} object + */ + public TempOutputStream createOutputStream() + { + return new TempOutputStream(tempDir, memoryThreshold, maxContentSize, encrypt, deleteTempFileOnClose); + } + + public File getTempDir() + { + return tempDir; + } + + public int getMemoryThreshold() + { + return memoryThreshold; + } + + public long getMaxContentSize() + { + return maxContentSize; + } + + public boolean isEncrypt() + { + return encrypt; + } + + public boolean isDeleteTempFileOnClose() + { + return deleteTempFileOnClose; + } +} diff --git a/src/main/java/org/alfresco/rest/framework/webscripts/AbstractResourceWebScript.java b/src/main/java/org/alfresco/rest/framework/webscripts/AbstractResourceWebScript.java index 06096e4154..a6b3285226 100644 --- a/src/main/java/org/alfresco/rest/framework/webscripts/AbstractResourceWebScript.java +++ b/src/main/java/org/alfresco/rest/framework/webscripts/AbstractResourceWebScript.java @@ -1,283 +1,309 @@ -/* - * #%L - * Alfresco Remote API - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ -package org.alfresco.rest.framework.webscripts; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.metrics.rest.RestMetricsReporter; -import org.alfresco.repo.tenant.TenantUtil; -import org.alfresco.repo.transaction.RetryingTransactionHelper; -import org.alfresco.repo.web.scripts.content.ContentStreamer; -import org.alfresco.rest.framework.Api; -import org.alfresco.rest.framework.core.HttpMethodSupport; -import org.alfresco.rest.framework.core.ResourceInspector; -import org.alfresco.rest.framework.core.ResourceLocator; -import org.alfresco.rest.framework.core.ResourceOperation; -import org.alfresco.rest.framework.core.ResourceWithMetadata; -import org.alfresco.rest.framework.core.exceptions.ApiException; -import org.alfresco.rest.framework.resource.actions.ActionExecutor; -import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction; -import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceBinaryAction; -import org.alfresco.rest.framework.resource.content.BinaryResource; -import org.alfresco.rest.framework.resource.content.CacheDirective; -import org.alfresco.rest.framework.resource.content.ContentInfo; -import org.alfresco.rest.framework.resource.content.FileBinaryResource; -import org.alfresco.rest.framework.resource.content.NodeBinaryResource; -import org.alfresco.rest.framework.resource.parameters.Params; -import org.alfresco.rest.framework.tools.ResponseWriter; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.extensions.webscripts.WebScriptException; -import org.springframework.extensions.webscripts.WebScriptRequest; -import org.springframework.extensions.webscripts.WebScriptResponse; -import org.springframework.http.HttpMethod; - -/** - * Webscript that handles the request for and execution of a Resource - * - * 1) Finds a resource - * 2) Extracts params - * 3) Executes params on a resource - * 4) Post processes the response to add embeds or projected relationship - * 5) Renders the response - * - * @author Gethin James - * @author janv - */ -// TODO for requests that pass in input streams e.g. binary content for workflow, this is going to need a way to re-read the input stream a la -// code in RepositoryContainer due to retrying transaction logic -public abstract class AbstractResourceWebScript extends ApiWebScript implements HttpMethodSupport, ActionExecutor, ResponseWriter -{ - private static Log logger = LogFactory.getLog(AbstractResourceWebScript.class); - - protected ResourceLocator locator; - private HttpMethod httpMethod; - private ParamsExtractor paramsExtractor; - private ContentStreamer streamer; - protected ResourceWebScriptHelper helper; - - @SuppressWarnings("rawtypes") - @Override - public void execute(final Api api, final WebScriptRequest req, final WebScriptResponse res) throws IOException - { - long startTime = System.currentTimeMillis(); - - try - { - final Map respons = new HashMap(); - final Map templateVars = req.getServiceMatch().getTemplateVars(); - final ResourceWithMetadata resource = locator.locateResource(api,templateVars, httpMethod); - final Params params = paramsExtractor.extractParams(resource.getMetaData(),req); - final boolean isReadOnly = HttpMethod.GET==httpMethod; - - //This execution usually takes place in a Retrying Transaction (see subclasses) - final Object toSerialize = execute(resource, params, res, isReadOnly); - - //Outside the transaction. - if (toSerialize != null) - { - if (toSerialize instanceof BinaryResource) - { - // TODO review (experimental) - can we move earlier & wrap complete execute ? Also for QuickShare (in MT/Cloud) needs to be tenant for the nodeRef (TBC). - boolean noAuth = false; - - if (BinaryResourceAction.Read.class.isAssignableFrom(resource.getResource().getClass())) - { - noAuth = resource.getMetaData().isNoAuth(BinaryResourceAction.Read.class); - } - else if (RelationshipResourceBinaryAction.Read.class.isAssignableFrom(resource.getResource().getClass())) - { - noAuth = resource.getMetaData().isNoAuth(RelationshipResourceBinaryAction.Read.class); - } - else - { - logger.warn("Unexpected"); - } - - if (noAuth) - { - String networkTenantDomain = TenantUtil.getCurrentDomain(); - - TenantUtil.runAsSystemTenant(new TenantUtil.TenantRunAsWork() - { - public Void doWork() throws Exception - { - streamResponse(req, res, (BinaryResource) toSerialize); - return null; - } - }, networkTenantDomain); - } - else - { - streamResponse(req, res, (BinaryResource) toSerialize); - } - } - else - { - renderJsonResponse(res, toSerialize, assistant.getJsonHelper()); - } - } - - } - catch (AlfrescoRuntimeException | ApiException | WebScriptException xception ) - { - renderException(xception, res, assistant); - } - catch (RuntimeException runtimeException) - { - renderException(runtimeException, res, assistant); - } - finally - { - reportExecutionTimeMetric(startTime, req.getServicePath()); - } - } - - public Object execute(final ResourceWithMetadata resource, final Params params, final WebScriptResponse res, boolean isReadOnly) - { - final String entityCollectionName = ResourceInspector.findEntityCollectionNameName(resource.getMetaData()); - final ResourceOperation operation = resource.getMetaData().getOperation(getHttpMethod()); - final WithResponse callBack = new WithResponse(operation.getSuccessStatus(), DEFAULT_JSON_CONTENT,CACHE_NEVER); - - // MNT-20308 - allow write transactions for authentication api - RetryingTransactionHelper transHelper = getTransactionHelper(resource.getMetaData().getApi().getName()); - - Object toReturn = transHelper.doInTransaction( - new RetryingTransactionHelper.RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - - Object result = executeAction(resource, params, callBack); - if (result instanceof BinaryResource) - { - return result; //don't postprocess it. - } - return helper.processAdditionsToTheResponse(res, resource.getMetaData().getApi(), entityCollectionName, params, result); - } - }, isReadOnly, true); - setResponse(res,callBack); - return toReturn; - } - - protected RetryingTransactionHelper getTransactionHelper(String api) - { - RetryingTransactionHelper transHelper = transactionService.getRetryingTransactionHelper(); - if (api.equals("authentication")) - { - transHelper.setForceWritable(true); - } - return transHelper; - } - - protected void streamResponse(final WebScriptRequest req, final WebScriptResponse res, BinaryResource resource) throws IOException - { - if (resource instanceof FileBinaryResource) - { - FileBinaryResource fileResource = (FileBinaryResource) resource; - // if requested, set attachment - boolean attach = StringUtils.isNotEmpty(fileResource.getAttachFileName()); - Map model = getModelForCacheDirective(fileResource.getCacheDirective()); - streamer.streamContent(req, res, fileResource.getFile(), null, attach, fileResource.getAttachFileName(), model); - } - else if (resource instanceof NodeBinaryResource) - { - NodeBinaryResource nodeResource = (NodeBinaryResource) resource; - ContentInfo contentInfo = nodeResource.getContentInfo(); - setContentInfoOnResponse(res, contentInfo); - // if requested, set attachment - boolean attach = StringUtils.isNotEmpty(nodeResource.getAttachFileName()); - Map model = getModelForCacheDirective(nodeResource.getCacheDirective()); - streamer.streamContent(req, res, nodeResource.getNodeRef(), nodeResource.getPropertyQName(), attach, nodeResource.getAttachFileName(), model); - } - - } - - private void reportExecutionTimeMetric(final long startTime, final String servicePath) - { - try - { - final RestMetricsReporter restMetricsReporter = assistant.getRestMetricsReporter(); - if (restMetricsReporter != null) - { - long delta = System.currentTimeMillis() - startTime; - restMetricsReporter.reportRestRequestExecutionTime(delta, httpMethod.toString(), servicePath); - } - } - catch (Exception e) - { - if (logger.isDebugEnabled()) - { - logger.debug("Could not report rest api metric:" + e.getMessage(), e); - } - } - } - - private static Map getModelForCacheDirective(CacheDirective cacheDirective) - { - if (cacheDirective != null) - { - return Collections.singletonMap(ContentStreamer.KEY_CACHE_DIRECTIVE, (Object) cacheDirective); - } - return null; - } - - public void setLocator(ResourceLocator locator) - { - this.locator = locator; - } - - public void setHttpMethod(HttpMethod httpMethod) - { - this.httpMethod = httpMethod; - } - - public void setParamsExtractor(ParamsExtractor paramsExtractor) - { - this.paramsExtractor = paramsExtractor; - } - - public void setHelper(ResourceWebScriptHelper helper) - { - this.helper = helper; - } - - public HttpMethod getHttpMethod() - { - return this.httpMethod; - } - - public void setStreamer(ContentStreamer streamer) - { - this.streamer = streamer; - } -} +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.framework.webscripts; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.metrics.rest.RestMetricsReporter; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.web.scripts.BufferedRequest; +import org.alfresco.repo.web.scripts.content.ContentStreamer; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.core.HttpMethodSupport; +import org.alfresco.rest.framework.core.ResourceInspector; +import org.alfresco.rest.framework.core.ResourceLocator; +import org.alfresco.rest.framework.core.ResourceOperation; +import org.alfresco.rest.framework.core.ResourceWithMetadata; +import org.alfresco.rest.framework.core.exceptions.ApiException; +import org.alfresco.rest.framework.resource.actions.ActionExecutor; +import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceBinaryAction; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.content.CacheDirective; +import org.alfresco.rest.framework.resource.content.ContentInfo; +import org.alfresco.rest.framework.resource.content.FileBinaryResource; +import org.alfresco.rest.framework.resource.content.NodeBinaryResource; +import org.alfresco.rest.framework.resource.parameters.Params; +import org.alfresco.rest.framework.tools.ResponseWriter; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.http.HttpMethod; + +/** + * Webscript that handles the request for and execution of a Resource + * + * 1) Finds a resource + * 2) Extracts params + * 3) Executes params on a resource + * 4) Post processes the response to add embeds or projected relationship + * 5) Renders the response + * + * @author Gethin James + * @author janv + */ +// TODO for requests that pass in input streams e.g. binary content for workflow, this is going to need a way to re-read the input stream a la +// code in RepositoryContainer due to retrying transaction logic +public abstract class AbstractResourceWebScript extends ApiWebScript implements HttpMethodSupport, ActionExecutor, ResponseWriter +{ + private static Log logger = LogFactory.getLog(AbstractResourceWebScript.class); + + protected ResourceLocator locator; + private HttpMethod httpMethod; + private ParamsExtractor paramsExtractor; + private ContentStreamer streamer; + protected ResourceWebScriptHelper helper; + + @SuppressWarnings("rawtypes") + @Override + public void execute(final Api api, final WebScriptRequest req, final WebScriptResponse res) throws IOException + { + long startTime = System.currentTimeMillis(); + + try + { + final Map templateVars = req.getServiceMatch().getTemplateVars(); + final ResourceWithMetadata resource = locator.locateResource(api,templateVars, httpMethod); + final boolean isReadOnly = HttpMethod.GET==httpMethod; + + // MNT-20308 - allow write transactions for authentication api + RetryingTransactionHelper transHelper = getTransactionHelper(resource.getMetaData().getApi().getName()); + + // encapsulate script within transaction + RetryingTransactionHelper.RetryingTransactionCallback work = new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + try + { + final Params params = paramsExtractor.extractParams(resource.getMetaData(), req); + return AbstractResourceWebScript.this.execute(resource, params, res, isReadOnly); + } + catch (Exception e) + { + if (req instanceof BufferedRequest) + { + // Reset the request in case of a transaction retry + ((BufferedRequest) req).reset(); + } + + // re-throw original exception for retry + throw e; + } + } + }; + + //This execution usually takes place in a Retrying Transaction (see subclasses) + final Object toSerialize = transHelper.doInTransaction(work, isReadOnly, true); + + //Outside the transaction. + if (toSerialize != null) + { + if (toSerialize instanceof BinaryResource) + { + // TODO review (experimental) - can we move earlier & wrap complete execute ? Also for QuickShare (in MT/Cloud) needs to be tenant for the nodeRef (TBC). + boolean noAuth = false; + + if (BinaryResourceAction.Read.class.isAssignableFrom(resource.getResource().getClass())) + { + noAuth = resource.getMetaData().isNoAuth(BinaryResourceAction.Read.class); + } + else if (RelationshipResourceBinaryAction.Read.class.isAssignableFrom(resource.getResource().getClass())) + { + noAuth = resource.getMetaData().isNoAuth(RelationshipResourceBinaryAction.Read.class); + } + else + { + logger.warn("Unexpected"); + } + + if (noAuth) + { + String networkTenantDomain = TenantUtil.getCurrentDomain(); + + TenantUtil.runAsSystemTenant(new TenantUtil.TenantRunAsWork() + { + public Void doWork() throws Exception + { + streamResponse(req, res, (BinaryResource) toSerialize); + return null; + } + }, networkTenantDomain); + } + else + { + streamResponse(req, res, (BinaryResource) toSerialize); + } + } + else + { + renderJsonResponse(res, toSerialize, assistant.getJsonHelper()); + } + } + + } + catch (AlfrescoRuntimeException | ApiException | WebScriptException xception ) + { + renderException(xception, res, assistant); + } + catch (RuntimeException runtimeException) + { + renderException(runtimeException, res, assistant); + } + finally + { + reportExecutionTimeMetric(startTime, req.getServicePath()); + } + } + + public Object execute(final ResourceWithMetadata resource, final Params params, final WebScriptResponse res, boolean isReadOnly) + { + final String entityCollectionName = ResourceInspector.findEntityCollectionNameName(resource.getMetaData()); + final ResourceOperation operation = resource.getMetaData().getOperation(getHttpMethod()); + final WithResponse callBack = new WithResponse(operation.getSuccessStatus(), DEFAULT_JSON_CONTENT,CACHE_NEVER); + + // MNT-20308 - allow write transactions for authentication api + RetryingTransactionHelper transHelper = getTransactionHelper(resource.getMetaData().getApi().getName()); + + Object toReturn = transHelper.doInTransaction( + new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + + Object result = executeAction(resource, params, callBack); + if (result instanceof BinaryResource) + { + return result; //don't postprocess it. + } + return helper.processAdditionsToTheResponse(res, resource.getMetaData().getApi(), entityCollectionName, params, result); + } + }, isReadOnly, false); + setResponse(res,callBack); + return toReturn; + } + + protected RetryingTransactionHelper getTransactionHelper(String api) + { + RetryingTransactionHelper transHelper = transactionService.getRetryingTransactionHelper(); + if (api.equals("authentication")) + { + transHelper.setForceWritable(true); + } + return transHelper; + } + + protected void streamResponse(final WebScriptRequest req, final WebScriptResponse res, BinaryResource resource) throws IOException + { + if (resource instanceof FileBinaryResource) + { + FileBinaryResource fileResource = (FileBinaryResource) resource; + // if requested, set attachment + boolean attach = StringUtils.isNotEmpty(fileResource.getAttachFileName()); + Map model = getModelForCacheDirective(fileResource.getCacheDirective()); + streamer.streamContent(req, res, fileResource.getFile(), null, attach, fileResource.getAttachFileName(), model); + } + else if (resource instanceof NodeBinaryResource) + { + NodeBinaryResource nodeResource = (NodeBinaryResource) resource; + ContentInfo contentInfo = nodeResource.getContentInfo(); + setContentInfoOnResponse(res, contentInfo); + // if requested, set attachment + boolean attach = StringUtils.isNotEmpty(nodeResource.getAttachFileName()); + Map model = getModelForCacheDirective(nodeResource.getCacheDirective()); + streamer.streamContent(req, res, nodeResource.getNodeRef(), nodeResource.getPropertyQName(), attach, nodeResource.getAttachFileName(), model); + } + + } + + private void reportExecutionTimeMetric(final long startTime, final String servicePath) + { + try + { + final RestMetricsReporter restMetricsReporter = assistant.getRestMetricsReporter(); + if (restMetricsReporter != null) + { + long delta = System.currentTimeMillis() - startTime; + restMetricsReporter.reportRestRequestExecutionTime(delta, httpMethod.toString(), servicePath); + } + } + catch (Exception e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Could not report rest api metric:" + e.getMessage(), e); + } + } + } + + private static Map getModelForCacheDirective(CacheDirective cacheDirective) + { + if (cacheDirective != null) + { + return Collections.singletonMap(ContentStreamer.KEY_CACHE_DIRECTIVE, (Object) cacheDirective); + } + return null; + } + + public void setLocator(ResourceLocator locator) + { + this.locator = locator; + } + + public void setHttpMethod(HttpMethod httpMethod) + { + this.httpMethod = httpMethod; + } + + public void setParamsExtractor(ParamsExtractor paramsExtractor) + { + this.paramsExtractor = paramsExtractor; + } + + public void setHelper(ResourceWebScriptHelper helper) + { + this.helper = helper; + } + + public HttpMethod getHttpMethod() + { + return this.httpMethod; + } + + public void setStreamer(ContentStreamer streamer) + { + this.streamer = streamer; + } +} diff --git a/src/main/java/org/alfresco/rest/framework/webscripts/ApiWebScript.java b/src/main/java/org/alfresco/rest/framework/webscripts/ApiWebScript.java index cbad461112..a9fa4eea83 100644 --- a/src/main/java/org/alfresco/rest/framework/webscripts/ApiWebScript.java +++ b/src/main/java/org/alfresco/rest/framework/webscripts/ApiWebScript.java @@ -1,147 +1,149 @@ -/* - * #%L - * Alfresco Remote API - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ -package org.alfresco.rest.framework.webscripts; - -import java.io.File; -import java.io.IOException; -import java.util.Map; - -import org.alfresco.repo.web.scripts.BufferedRequest; -import org.alfresco.repo.web.scripts.BufferedResponse; -import org.alfresco.rest.framework.Api; -import org.alfresco.rest.framework.tools.ApiAssistant; -import org.alfresco.service.transaction.TransactionService; -import org.alfresco.util.TempFileProvider; -import org.apache.chemistry.opencmis.server.shared.TempStoreOutputStreamFactory; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.extensions.webscripts.AbstractWebScript; -import org.springframework.extensions.webscripts.WebScriptRequest; -import org.springframework.extensions.webscripts.WebScriptResponse; - -/** - * Entry point for API webscript. Supports version/scope as well - * as discovery. - * - * @author Gethin James - */ -public abstract class ApiWebScript extends AbstractWebScript -{ - private static Log logger = LogFactory.getLog(ApiWebScript.class); - protected ApiAssistant assistant; - protected boolean encryptTempFiles = false; - protected String tempDirectoryName = null; - protected int memoryThreshold = 4 * 1024 * 1024; // 4mb - protected long maxContentSize = (long) 4 * 1024 * 1024 * 1024; // 4gb - protected TempStoreOutputStreamFactory streamFactory = null; - protected TransactionService transactionService; - - public void setTransactionService(TransactionService transactionService) - { - this.transactionService = transactionService; - } - - public void setAssistant(ApiAssistant assistant) { - this.assistant = assistant; - } - - public void setTempDirectoryName(String tempDirectoryName) - { - this.tempDirectoryName = tempDirectoryName; - } - - public void setEncryptTempFiles(boolean encryptTempFiles) - { - this.encryptTempFiles = encryptTempFiles; - } - - public void setMemoryThreshold(int memoryThreshold) - { - this.memoryThreshold = memoryThreshold; - } - - public void setMaxContentSize(long maxContentSize) - { - this.maxContentSize = maxContentSize; - } - - public void setStreamFactory(TempStoreOutputStreamFactory streamFactory) - { - this.streamFactory = streamFactory; - } - - public void init() - { - File tempDirectory = TempFileProvider.getTempDir(tempDirectoryName); - this.streamFactory = TempStoreOutputStreamFactory.newInstance(tempDirectory, memoryThreshold, maxContentSize, encryptTempFiles); - } - - @Override - public void execute(final WebScriptRequest req, final WebScriptResponse res) throws IOException - { - Map templateVars = req.getServiceMatch().getTemplateVars(); - Api api = assistant.determineApi(templateVars); - - final BufferedRequest bufferedReq = getRequest(req); - final BufferedResponse bufferedRes = getResponse(res); - - try - { - execute(api, bufferedReq, bufferedRes); - } - finally - { - // Get rid of any temporary files - if (bufferedReq != null) - { - bufferedReq.close(); - } - } - - // Ensure a response is always flushed after successful execution - if (bufferedRes != null) - { - bufferedRes.writeResponse(); - } - } - - protected BufferedRequest getRequest(final WebScriptRequest req) - { - // create buffered request and response that allow transaction retrying - final BufferedRequest bufferedReq = new BufferedRequest(req, streamFactory); - return bufferedReq; - } - - protected BufferedResponse getResponse(final WebScriptResponse resp) - { - // create buffered request and response that allow transaction retrying - final BufferedResponse bufferedRes = new BufferedResponse(resp, memoryThreshold, streamFactory); - return bufferedRes; - } - - public abstract void execute(final Api api, WebScriptRequest req, WebScriptResponse res) throws IOException; - -} +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.framework.webscripts; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +import org.alfresco.repo.web.scripts.BufferedRequest; +import org.alfresco.repo.web.scripts.BufferedResponse; +import org.alfresco.repo.web.scripts.TempOutputStreamFactory; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.tools.ApiAssistant; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * Entry point for API webscript. Supports version/scope as well + * as discovery. + * + * @author Gethin James + */ +public abstract class ApiWebScript extends AbstractWebScript +{ + private static Log logger = LogFactory.getLog(ApiWebScript.class); + protected ApiAssistant assistant; + protected boolean encryptTempFiles = false; + protected String tempDirectoryName = null; + protected int memoryThreshold = 4 * 1024 * 1024; // 4mb + protected long maxContentSize = (long) 4 * 1024 * 1024 * 1024; // 4gb + protected TempOutputStreamFactory streamFactory = null; + protected TempOutputStreamFactory responseStreamFactory = null; + protected TransactionService transactionService; + + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + public void setAssistant(ApiAssistant assistant) { + this.assistant = assistant; + } + + public void setTempDirectoryName(String tempDirectoryName) + { + this.tempDirectoryName = tempDirectoryName; + } + + public void setEncryptTempFiles(boolean encryptTempFiles) + { + this.encryptTempFiles = encryptTempFiles; + } + + public void setMemoryThreshold(int memoryThreshold) + { + this.memoryThreshold = memoryThreshold; + } + + public void setMaxContentSize(long maxContentSize) + { + this.maxContentSize = maxContentSize; + } + + public void setStreamFactory(TempOutputStreamFactory streamFactory) + { + this.streamFactory = streamFactory; + } + + public void init() + { + File tempDirectory = TempFileProvider.getTempDir(tempDirectoryName); + this.streamFactory = new TempOutputStreamFactory(tempDirectory, memoryThreshold, maxContentSize, false, false); + this.responseStreamFactory = new TempOutputStreamFactory(tempDirectory, memoryThreshold, maxContentSize, false, true); + } + + @Override + public void execute(final WebScriptRequest req, final WebScriptResponse res) throws IOException + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + Api api = assistant.determineApi(templateVars); + + final BufferedRequest bufferedReq = getRequest(req); + final BufferedResponse bufferedRes = getResponse(res); + + try + { + execute(api, bufferedReq, bufferedRes); + } + finally + { + // Get rid of any temporary files + if (bufferedReq != null) + { + bufferedReq.close(); + } + } + + // Ensure a response is always flushed after successful execution + if (bufferedRes != null) + { + bufferedRes.writeResponse(); + } + } + + protected BufferedRequest getRequest(final WebScriptRequest req) + { + // create buffered request and response that allow transaction retrying + final BufferedRequest bufferedReq = new BufferedRequest(req, streamFactory); + return bufferedReq; + } + + protected BufferedResponse getResponse(final WebScriptResponse resp) + { + // create buffered request and response that allow transaction retrying + final BufferedResponse bufferedRes = new BufferedResponse(resp, memoryThreshold, streamFactory); + return bufferedRes; + } + + public abstract void execute(final Api api, WebScriptRequest req, WebScriptResponse res) throws IOException; + +} diff --git a/src/test/java/org/alfresco/AppContext02TestSuite.java b/src/test/java/org/alfresco/AppContext02TestSuite.java index 5f7ddeb101..9d18221209 100644 --- a/src/test/java/org/alfresco/AppContext02TestSuite.java +++ b/src/test/java/org/alfresco/AppContext02TestSuite.java @@ -67,7 +67,8 @@ import org.junit.runners.Suite; org.alfresco.rest.api.tests.TestPublicApiCaching.class, org.alfresco.rest.api.tests.TestUserPreferences.class, org.alfresco.rest.api.tests.WherePredicateApiTest.class, - org.alfresco.rest.api.tests.TestRemovePermissions.class, + org.alfresco.rest.api.tests.TestRemovePermissions.class, + org.alfresco.rest.api.tests.TempOutputStreamTest.class, org.alfresco.rest.api.tests.BufferedResponseTest.class, org.alfresco.rest.workflow.api.tests.DeploymentWorkflowApiTest.class, org.alfresco.rest.workflow.api.tests.ProcessDefinitionWorkflowApiTest.class, diff --git a/src/test/java/org/alfresco/rest/api/tests/ApiTest.java b/src/test/java/org/alfresco/rest/api/tests/ApiTest.java index 64cce3b353..75375d53df 100644 --- a/src/test/java/org/alfresco/rest/api/tests/ApiTest.java +++ b/src/test/java/org/alfresco/rest/api/tests/ApiTest.java @@ -1,90 +1,91 @@ -/* - * #%L - * Alfresco Remote API - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ -package org.alfresco.rest.api.tests; - - -import org.alfresco.rest.DeletedNodesTest; -import org.alfresco.rest.api.search.BasicSearchApiIntegrationTest; -import org.junit.AfterClass; -import org.junit.runner.RunWith; -import org.junit.runners.Suite; - -/** - * Public V1 REST API tests - * - * @author steveglover - * @author janv - * @author Jamal Kaabi-Mofrad - * @author Gethin James - * - */ -@RunWith(Suite.class) -@Suite.SuiteClasses({ - - NodeApiTest.class, - NodeAssociationsApiTest.class, - NodeVersionsApiTest.class, - BasicSearchApiIntegrationTest.class, - QueriesNodesApiTest.class, - QueriesPeopleApiTest.class, - QueriesSitesApiTest.class, - RenditionsTest.class, - SharedLinkApiTest.class, - ActivitiesPostingTest.class, - DeletedNodesTest.class, - AuthenticationsTest.class, - ModulePackagesApiTest.class, - WherePredicateApiTest.class, - DiscoveryApiTest.class, - TestSites.class, - TestNodeComments.class, - TestFavouriteSites.class, - TestSiteContainers.class, - TestNodeRatings.class, - TestUserPreferences.class, - TestTags.class, - TestNetworks.class, - TestActivities.class, - GroupsTest.class, - TestPeople.class, - TestSiteMembers.class, - TestPersonSites.class, - TestSiteMembershipRequests.class, - TestFavourites.class, - TestPublicApi128.class, - TestPublicApiCaching.class, - TestDownloads.class, - AuditAppTest.class, - BufferedResponseTest.class -}) -public class ApiTest -{ - @AfterClass - public static void after() throws Exception - { -// EnterprisePublicApiTestFixture.cleanup(); - } -} +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.tests; + + +import org.alfresco.rest.DeletedNodesTest; +import org.alfresco.rest.api.search.BasicSearchApiIntegrationTest; +import org.junit.AfterClass; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +/** + * Public V1 REST API tests + * + * @author steveglover + * @author janv + * @author Jamal Kaabi-Mofrad + * @author Gethin James + * + */ +@RunWith(Suite.class) +@Suite.SuiteClasses({ + + NodeApiTest.class, + NodeAssociationsApiTest.class, + NodeVersionsApiTest.class, + BasicSearchApiIntegrationTest.class, + QueriesNodesApiTest.class, + QueriesPeopleApiTest.class, + QueriesSitesApiTest.class, + RenditionsTest.class, + SharedLinkApiTest.class, + ActivitiesPostingTest.class, + DeletedNodesTest.class, + AuthenticationsTest.class, + ModulePackagesApiTest.class, + WherePredicateApiTest.class, + DiscoveryApiTest.class, + TestSites.class, + TestNodeComments.class, + TestFavouriteSites.class, + TestSiteContainers.class, + TestNodeRatings.class, + TestUserPreferences.class, + TestTags.class, + TestNetworks.class, + TestActivities.class, + GroupsTest.class, + TestPeople.class, + TestSiteMembers.class, + TestPersonSites.class, + TestSiteMembershipRequests.class, + TestFavourites.class, + TestPublicApi128.class, + TestPublicApiCaching.class, + TestDownloads.class, + AuditAppTest.class, + TempOutputStreamTest.class, + BufferedResponseTest.class +}) +public class ApiTest +{ + @AfterClass + public static void after() throws Exception + { +// EnterprisePublicApiTestFixture.cleanup(); + } +} diff --git a/src/test/java/org/alfresco/rest/api/tests/BufferedResponseTest.java b/src/test/java/org/alfresco/rest/api/tests/BufferedResponseTest.java index bbcf2a9772..39c86ed916 100644 --- a/src/test/java/org/alfresco/rest/api/tests/BufferedResponseTest.java +++ b/src/test/java/org/alfresco/rest/api/tests/BufferedResponseTest.java @@ -27,8 +27,9 @@ package org.alfresco.rest.api.tests; import org.alfresco.repo.web.scripts.BufferedResponse; +import org.alfresco.repo.web.scripts.TempOutputStream; +import org.alfresco.repo.web.scripts.TempOutputStreamFactory; import org.alfresco.util.TempFileProvider; -import org.apache.chemistry.opencmis.server.shared.TempStoreOutputStreamFactory; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -54,7 +55,7 @@ public class BufferedResponseTest private static final String TEMP_DIRECTORY_NAME = "testLargeFile"; private static final String LARGE_FILE_NAME = "largeFile.tmp"; - private static final String FILE_PREFIX = "opencmis"; + private static final String FILE_PREFIX = TempOutputStream.TEMP_FILE_PREFIX; private static final Integer LARGE_FILE_SIZE_BYTES = 5 * 1024 * 1024; private static final Integer MEMORY_THRESHOLD = 4 * 1024 * 1024; @@ -81,12 +82,14 @@ public class BufferedResponseTest public void testOutputStream() throws IOException { File bufferTempDirectory = TempFileProvider.getTempDir(TEMP_DIRECTORY_NAME); - TempStoreOutputStreamFactory streamFactory = TempStoreOutputStreamFactory.newInstance(bufferTempDirectory, MEMORY_THRESHOLD, MAX_CONTENT_SIZE,false); + TempOutputStreamFactory streamFactory = new TempOutputStreamFactory(bufferTempDirectory, MEMORY_THRESHOLD, MAX_CONTENT_SIZE, false,true); BufferedResponse response = new BufferedResponse(null, 0, streamFactory); long countBefore = countFilesInDirectoryWithPrefix(bufferTempDirectory, FILE_PREFIX ); copyFileToOutputStream(response); long countAfter = countFilesInDirectoryWithPrefix(bufferTempDirectory, FILE_PREFIX); + + response.getOutputStream().close(); Assert.assertEquals(countBefore + 1, countAfter); diff --git a/src/test/java/org/alfresco/rest/api/tests/NodeApiTest.java b/src/test/java/org/alfresco/rest/api/tests/NodeApiTest.java index e02dfe146e..2220d96f75 100644 --- a/src/test/java/org/alfresco/rest/api/tests/NodeApiTest.java +++ b/src/test/java/org/alfresco/rest/api/tests/NodeApiTest.java @@ -37,6 +37,7 @@ import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; import java.io.File; +import java.io.RandomAccessFile; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; @@ -2783,6 +2784,131 @@ public class NodeApiTest extends AbstractSingleNetworkSiteTest post(postUrl, toJsonAsStringNonNull(d1), "?"+Nodes.PARAM_AUTO_RENAME+"=false", 409); } + @Test + public void testUpdateNodeConcurrentlyUsingInMemoryBacked() throws Exception + { + // Less than its memory threshold ( 4 MB ) + updateNodeConcurrently(1024L); + } + + @Test + public void testUpdateNodeConcurrentlyUsingFileBacked() throws Exception + { + // Bigger than its memory threshold ( 5 > 4 MB ) + updateNodeConcurrently(5 * 1024 * 1024L); + } + + private void updateNodeConcurrently(Long contentSize) throws Exception + { + setRequestContext(user1); + + // Create folder + String folder0Name = "f0-testUpdateNodeConcurrently-" + RUNID; + String f0Id = createFolder(Nodes.PATH_MY, folder0Name).getId(); + + // Create empty file + Document d1 = new Document(); + d1.setName("d1.txt"); + d1.setNodeType(TYPE_CM_CONTENT); + + Map params = new HashMap<>(); + params.put("majorVersion", "true"); + + Document documentResp = createEmptyTextFile(f0Id, d1.getName(), params, 201); + assertEquals("1.0", documentResp.getProperties().get("cm:versionLabel")); + String docId = documentResp.getId(); + + // Store the threads so that we can check if they are done + List threads = new ArrayList(); + + // Create threads + for (int i = 0; i < 2; i++) + { + Runnable task = new UpdateNodeRunnable(docId, contentSize); + + Thread worker = new Thread(task); + worker.setName(String.valueOf(i)); + worker.start(); + + // Remember the thread for later usage + threads.add(worker); + } + int running = 0; + do + { + running = 0; + for (Thread thread : threads) + { + if (thread.isAlive()) + { + running++; + } + } + } while (running > 0); + + HttpResponse response = getSingle(URL_NODES, docId, 200); + documentResp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class); + + assertTrue("File size is 0 bytes", documentResp.getContent().getSizeInBytes().intValue() > 0); + } + + private class UpdateNodeRunnable implements Runnable + { + private final String docId; + private final Long contentSize; + + UpdateNodeRunnable(String docId, Long contentSize) + { + this.docId = docId; + this.contentSize = contentSize; + } + + @Override + public void run() + { + setRequestContext(user1); + + Map params = new HashMap<>(); + params.put("majorVersion", "true"); + + Document documentResp = null; + try + { + documentResp = updateTextFileWithRandomContent(docId, contentSize, params); + } + catch (Exception e) + { + e.printStackTrace(); + } + assertTrue(documentResp.getAspectNames().contains("cm:versionable")); + assertNotNull(documentResp.getProperties()); + + assertEquals(contentSize, documentResp.getContent().getSizeInBytes()); + } + } + + protected Document updateTextFileWithRandomContent(String contentId, Long contentSize, Map params) throws Exception + { + return updateTextFileWithRandomContent(contentId, contentSize, params, 200); + } + + protected Document updateTextFileWithRandomContent(String contentId, Long contentSize, Map params, int expectedStatus) throws Exception + { + File txtFile = TempFileProvider.createTempFile(getClass().getSimpleName(), ".txt"); + RandomAccessFile file = new RandomAccessFile(txtFile.getPath(), "rw"); + file.setLength(contentSize); + file.close(); + + BinaryPayload payload = new BinaryPayload(txtFile); + + HttpResponse response = putBinary(getNodeContentUrl(contentId), payload, null, params, expectedStatus); + if (expectedStatus != 200) + { + return null; + } + return RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class); + } + /** * Tests update node info (file or folder) *

PUT:

diff --git a/src/test/java/org/alfresco/rest/api/tests/TempOutputStreamTest.java b/src/test/java/org/alfresco/rest/api/tests/TempOutputStreamTest.java new file mode 100644 index 0000000000..ae7b475f99 --- /dev/null +++ b/src/test/java/org/alfresco/rest/api/tests/TempOutputStreamTest.java @@ -0,0 +1,252 @@ + +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.tests; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.stream.Stream; + +import org.alfresco.repo.content.ContentLimitViolationException; +import org.alfresco.repo.web.scripts.TempOutputStream; +import org.alfresco.repo.web.scripts.TempOutputStreamFactory; +import org.alfresco.util.TempFileProvider; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.util.StreamUtils; + +/** + * Tests basic {@link TempOutputStream} functionality + */ +public class TempOutputStreamTest +{ + private static final String TEMP_DIRECTORY_NAME = "TempOutputStreamTest"; + private static final String FILE_PREFIX = TempOutputStream.TEMP_FILE_PREFIX; + private static final int MEMORY_THRESHOLD = 4 * 1024 * 1024; + private static final long MAX_CONTENT_SIZE = 1024 * 1024 * 1024; + private static final File bufferTempDirectory = TempFileProvider.getTempDir(TEMP_DIRECTORY_NAME); + + @Test + public void testInMemoryStream() throws IOException + { + TempOutputStreamFactory streamFactory = new TempOutputStreamFactory(bufferTempDirectory, MEMORY_THRESHOLD, MAX_CONTENT_SIZE, false, false); + + File file = createTextFileWithRandomContent(MEMORY_THRESHOLD - 1024L); + { + TempOutputStream outputStream = streamFactory.createOutputStream(); + + long countBefore = countFilesInDirectoryWithPrefix(bufferTempDirectory); + + // Copy the stream + StreamUtils.copy(new BufferedInputStream(new FileInputStream(file)), outputStream); + + long countAfter = countFilesInDirectoryWithPrefix(bufferTempDirectory); + + Assert.assertEquals(countBefore, countAfter); + outputStream.destroy(); + } + file.delete(); + } + + @Test + public void testFileBackedStream() throws IOException + { + File file = createTextFileWithRandomContent(MEMORY_THRESHOLD + 1024L); + + { + // Create stream factory that doesn't delete temp file on stream close + TempOutputStreamFactory streamFactory = new TempOutputStreamFactory(bufferTempDirectory, MEMORY_THRESHOLD, MAX_CONTENT_SIZE, false, false); + TempOutputStream outputStream = streamFactory.createOutputStream(); + + long countBefore = countFilesInDirectoryWithPrefix(bufferTempDirectory); + + StreamUtils.copy(new BufferedInputStream(new FileInputStream(file)), outputStream); + + // Check that temp file was created + long countAfter = countFilesInDirectoryWithPrefix(bufferTempDirectory); + Assert.assertEquals(countBefore + 1, countAfter); + + outputStream.close(); + + // Check that file wasn't deleted on output stream close + countAfter = countFilesInDirectoryWithPrefix(bufferTempDirectory); + Assert.assertEquals(countBefore + 1, countAfter); + + outputStream.destroy(); + + // Check that file was deleted + countAfter = countFilesInDirectoryWithPrefix(bufferTempDirectory); + Assert.assertEquals(countBefore, countAfter); + } + + { + // Create stream factory that deletes temp file on stream close + TempOutputStreamFactory streamFactory = new TempOutputStreamFactory(bufferTempDirectory, MEMORY_THRESHOLD, MAX_CONTENT_SIZE, false, true); + TempOutputStream outputStream = streamFactory.createOutputStream(); + + long countBefore = countFilesInDirectoryWithPrefix(bufferTempDirectory); + + StreamUtils.copy(new BufferedInputStream(new FileInputStream(file)), outputStream); + + // Check that temp file was created + long countAfter = countFilesInDirectoryWithPrefix(bufferTempDirectory); + Assert.assertEquals(countBefore + 1, countAfter); + + outputStream.close(); + + // Check that file was deleted on close + countAfter = countFilesInDirectoryWithPrefix(bufferTempDirectory); + Assert.assertEquals(countBefore, countAfter); + } + + file.delete(); + } + + @Test + public void testMaxContentSize() throws IOException + { + // In memory stream + { + long contentSize = MEMORY_THRESHOLD - 512; + long maxContentSize = MEMORY_THRESHOLD - 1024; + + File file = createTextFileWithRandomContent(contentSize); + + // Create stream factory that deletes temp file on stream close + TempOutputStreamFactory streamFactory = new TempOutputStreamFactory(bufferTempDirectory, MEMORY_THRESHOLD, maxContentSize, false, true); + TempOutputStream outputStream = streamFactory.createOutputStream(); + + long countBefore = countFilesInDirectoryWithPrefix(bufferTempDirectory); + + try + { + StreamUtils.copy(new BufferedInputStream(new FileInputStream(file)), outputStream); + Assert.fail("Content size limit violation exception was expected"); + } + catch (ContentLimitViolationException e) + { + // Expected + } + + // Check that file was already deleted on close + long countAfter = countFilesInDirectoryWithPrefix(bufferTempDirectory); + Assert.assertEquals(countBefore, countAfter); + + file.delete(); + } + + // File backed stream + { + long contentSize = MEMORY_THRESHOLD + 1024; + long maxContentSize = MEMORY_THRESHOLD + 512; + + File file = createTextFileWithRandomContent(contentSize); + + // Create stream factory that deletes temp file on stream close + TempOutputStreamFactory streamFactory = new TempOutputStreamFactory(bufferTempDirectory, MEMORY_THRESHOLD, maxContentSize, false, true); + TempOutputStream outputStream = streamFactory.createOutputStream(); + + long countBefore = countFilesInDirectoryWithPrefix(bufferTempDirectory); + + try + { + StreamUtils.copy(new BufferedInputStream(new FileInputStream(file)), outputStream); + Assert.fail("Content size limit violation exception was expected"); + } + catch (ContentLimitViolationException e) + { + // Expected + } + + // Check that file was already deleted on close + long countAfter = countFilesInDirectoryWithPrefix(bufferTempDirectory); + Assert.assertEquals(countBefore, countAfter); + + file.delete(); + } + } + + @Test + public void testEncryptContent() throws IOException + { + File file = createTextFileWithRandomContent(MEMORY_THRESHOLD + 1024L); + + // Create stream factory that doesn't delete temp file on stream close + TempOutputStreamFactory streamFactory = new TempOutputStreamFactory(bufferTempDirectory, MEMORY_THRESHOLD, MAX_CONTENT_SIZE, true, false); + + TempOutputStream outputStream = streamFactory.createOutputStream(); + + long countBefore = countFilesInDirectoryWithPrefix(bufferTempDirectory); + + StreamUtils.copy(new BufferedInputStream(new FileInputStream(file)), outputStream); + + // Check that temp file was created + long countAfter = countFilesInDirectoryWithPrefix(bufferTempDirectory); + Assert.assertEquals(countBefore + 1, countAfter); + + outputStream.close(); + + // Check that file wasn't deleted on output stream close + countAfter = countFilesInDirectoryWithPrefix(bufferTempDirectory); + Assert.assertEquals(countBefore + 1, countAfter); + + // Compare content + String contentWriten = StreamUtils.copyToString(new BufferedInputStream(new FileInputStream(file)), Charset.defaultCharset()); + String contentRead = StreamUtils.copyToString(outputStream.getInputStream(), Charset.defaultCharset()); + Assert.assertEquals(contentWriten, contentRead); + + outputStream.destroy(); + + // Check that file was deleted + countAfter = countFilesInDirectoryWithPrefix(bufferTempDirectory); + Assert.assertEquals(countBefore, countAfter); + + file.delete(); + } + + private File createTextFileWithRandomContent(long contentSize) throws IOException + { + File txtFile = TempFileProvider.createTempFile(getClass().getSimpleName(), ".txt"); + txtFile.deleteOnExit(); + + RandomAccessFile file = new RandomAccessFile(txtFile.getPath(), "rw"); + file.setLength(contentSize); + file.close(); + + return txtFile; + } + + private long countFilesInDirectoryWithPrefix(File directory) throws IOException + { + Stream fileStream = Arrays.stream(directory.listFiles()); + return fileStream.filter(f -> f.getName().startsWith(FILE_PREFIX)).count(); + } +}