Java tutorial
/* * #%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 <http://www.gnu.org/licenses/>. * #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.ThresholdOutputStreamFactory; 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 ThresholdOutputStreamFactory streamFactory = 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 = ThresholdOutputStreamFactory.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(); } } /** * @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<Class<?>> 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<Class<?>> 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<String, Object> getScriptParameters() { Map<String, Object> params = new HashMap<String, Object>(); params.putAll(super.getScriptParameters()); addRepoParameters(params); return params; } /* * (non-Javadoc) * @see org.alfresco.web.scripts.AbstractRuntimeContainer#getTemplateParameters() */ public Map<String, Object> getTemplateParameters() { // Ensure we have a transaction - we might be generating the status template after the main transaction failed return fallbackTransactionHelper.doInTransaction(new RetryingTransactionCallback<Map<String, Object>>() { public Map<String, Object> execute() throws Throwable { Map<String, Object> params = new HashMap<String, Object>(); 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<String, Object> */ private void addRepoParameters(Map<String, Object> 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<Boolean> authWork = new RetryingTransactionCallback<Boolean>() { 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()); } 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<Object> work = new RetryingTransactionCallback<Object>() { 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(); bufferedRes.reset(); 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<Object> work = new RunAsWork<Object>() { 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<Object> work = new RunAsWork<Object>() { 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<Object>() { public Object execute() throws Exception { internalReset(); return null; } }, true, false); } private void internalReset() { super.reset(); } }