Java tutorial
/* * Copyright 2005-2007 Open Source Applications Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.osaf.cosmo.cmp; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.xml.serialize.OutputFormat; import org.apache.xml.serialize.XMLSerializer; import org.hibernate.validator.InvalidStateException; import org.osaf.cosmo.model.DuplicateEmailException; import org.osaf.cosmo.model.DuplicateUsernameException; import org.osaf.cosmo.model.EntityFactory; import org.osaf.cosmo.model.HomeCollectionItem; import org.osaf.cosmo.model.ModelValidationException; import org.osaf.cosmo.model.PasswordRecovery; import org.osaf.cosmo.model.User; import org.osaf.cosmo.security.CosmoSecurityManager; import org.osaf.cosmo.service.OverlordDeletionException; import org.osaf.cosmo.service.ContentService; import org.osaf.cosmo.service.UserService; import org.osaf.cosmo.service.ServiceListener; import org.osaf.cosmo.service.account.AccountActivator; import org.osaf.cosmo.service.account.ActivationContext; import org.osaf.cosmo.service.account.ActivationListener; import org.osaf.cosmo.service.account.OutOfTheBoxContext; import org.osaf.cosmo.service.account.OutOfTheBoxHelper; import org.osaf.cosmo.service.account.OutOfTheBoxListener; import org.osaf.cosmo.service.account.PasswordRecoveryMessageContext; import org.osaf.cosmo.service.account.PasswordRecoverer; import org.osaf.cosmo.server.ServiceLocator; import org.osaf.cosmo.server.ServiceLocatorFactory; import org.osaf.cosmo.server.SpaceUsageReport; import org.osaf.cosmo.server.StatusSnapshot; import org.osaf.cosmo.server.UserPath; import org.osaf.cosmo.spring.CosmoPropertyPlaceholderConfigurer; import org.osaf.cosmo.util.PageCriteria; import org.springframework.beans.BeansException; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; import org.w3c.dom.Document; import org.xml.sax.SAXException; /** * Implements RESTful HTTP-based protocol for Cosmo management * operations. * * See * http://wiki.osafoundation.org/bin/view/Projects/CosmoManagementProtocol * for the protocol specification. */ public class CmpServlet extends HttpServlet { private static final Log log = LogFactory.getLog(CmpServlet.class); private static final DocumentBuilderFactory BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); private static final Pattern PATTERN_SPACE_USAGE = Pattern.compile("^/server/usage/space(/[^/]+)?(/xml)?$"); private static final String URL_PASSWORD_RESET = "/account/password/reset/"; private static final String URL_ACTIVATE = "/activate/"; private static final String BEAN_CONTENT_SERVICE = "contentService"; private static final String BEAN_USER_SERVICE = "userService"; private static final String BEAN_PASSWORD_RECOVERER = "passwordRecoverer"; private static final String BEAN_SERVICE_LOCATOR_FACTORY = "serviceLocatorFactory"; private static final String BEAN_SECURITY_MANAGER = "securityManager"; private static final String BEAN_ACCOUNT_ACTIVATOR = "accountActivator"; private static final String BEAN_OOTB_HELPER = "ootbHelper"; private static final String BEAN_ENTITY_FACTORY = "cosmoEntityFactory"; private static final String BEAN_PROPERTIES = "propertyPlaceholderConfigurer"; private static final String PROPERTY_CREATE_OOB_COLS = "cosmo.service.account.createOutOfBoxCollections"; private static final String PROPERTY_DISABLE_SIGNUPS = "cosmo.service.account.disableSignups"; private static final int DEFAULT_PAGE_NUMBER = 1; private static final int DEFAULT_PAGE_SIZE = PageCriteria.VIEW_ALL; private static final boolean DEFAULT_SORT_ASCENDING = true; private static final User.SortType DEFAULT_SORT_TYPE = User.SortType.USERNAME; private WebApplicationContext wac; private ContentService contentService; private UserService userService; private EntityFactory entityFactory; private ServiceLocatorFactory serviceLocatorFactory; private CosmoSecurityManager securityManager; private PasswordRecoverer passwordRecoverer; private AccountActivator accountActivator; private OutOfTheBoxHelper ootbHelper; private boolean createOutOfBoxCollections = false; private boolean disableSignups = false; /** * Loads the servlet context's <code>WebApplicationContext</code> * and wires up dependencies. If no * <code>WebApplicationContext</code> is found, dependencies must * be set manually (useful for testing). * * @throws ServletException */ public void init() throws ServletException { super.init(); wac = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); if (wac != null) { if (contentService == null) contentService = (ContentService) getBean(BEAN_CONTENT_SERVICE, ContentService.class); if (userService == null) userService = (UserService) getBean(BEAN_USER_SERVICE, UserService.class); if (securityManager == null) securityManager = (CosmoSecurityManager) getBean(BEAN_SECURITY_MANAGER, CosmoSecurityManager.class); if (serviceLocatorFactory == null) serviceLocatorFactory = (ServiceLocatorFactory) getBean(BEAN_SERVICE_LOCATOR_FACTORY, ServiceLocatorFactory.class); if (accountActivator == null) accountActivator = (AccountActivator) getBean(BEAN_ACCOUNT_ACTIVATOR, AccountActivator.class); if (passwordRecoverer == null) passwordRecoverer = (PasswordRecoverer) getBean(BEAN_PASSWORD_RECOVERER, PasswordRecoverer.class); if (ootbHelper == null) ootbHelper = (OutOfTheBoxHelper) getBean(BEAN_OOTB_HELPER, OutOfTheBoxHelper.class); if (entityFactory == null) entityFactory = (EntityFactory) getBean(BEAN_ENTITY_FACTORY, EntityFactory.class); // Determine if out-of-box collections should be created // by looking in properties CosmoPropertyPlaceholderConfigurer cppc = (CosmoPropertyPlaceholderConfigurer) getBean(BEAN_PROPERTIES, CosmoPropertyPlaceholderConfigurer.class); if (cppc != null) { String createOobCols = cppc.getProperties().getProperty(PROPERTY_CREATE_OOB_COLS); if (createOobCols != null && "true".equalsIgnoreCase(createOobCols)) createOutOfBoxCollections = true; String disableSignupsProp = cppc.getProperties().getProperty(PROPERTY_DISABLE_SIGNUPS); if (disableSignupsProp != null && "true".equalsIgnoreCase(disableSignupsProp)) disableSignups = true; } } if (contentService == null) throw new ServletException("content service must not be null"); if (userService == null) throw new ServletException("user service must not be null"); if (securityManager == null) throw new ServletException("security manager must not be null"); if (accountActivator == null) throw new ServletException("account activator must not be null"); if (passwordRecoverer == null) throw new ServletException("password recoverer must not be null"); if (ootbHelper == null) throw new ServletException("ootb helper must not be null"); } // HttpServlet methods /** * Responds to the following operations: * * <ul> * <li><code>DELETE /user/&lgt;username></code></li> * </ul> */ protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (req.getPathInfo().startsWith("/user/")) { processUserDelete(req, resp); return; } if (req.getPathInfo().equals("/account")) { processAccountDelete(req, resp); return; } resp.setStatus(HttpServletResponse.SC_NOT_FOUND); } /** * Responds to the following operations: * * <ul> * <li><code>GET /account</code></li> * <li><code>GET /users</code></li> * <li><code>GET /user/&lgt;username></code></li> * </ul> */ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Matcher m = null; // /account if (req.getPathInfo().equals("/account")) { processAccountGet(req, resp); return; } // /users if (req.getPathInfo().equals("/users")) { processUsersGet(req, resp); return; } if (req.getPathInfo().equals("/users/count")) { processGetUserCount(req, resp); return; } // /user UserPath up = UserPath.parse(req.getPathInfo(), true); if (up != null) { User user = userService.getUser(up.getUsername()); if (user == null) { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } if (up.getPathInfo() != null) { // /user/<username>/something-else if (!up.getPathInfo().equals("/service")) { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } // /user/<username>/service if (!(securityManager.getSecurityContext().isAdmin() || securityManager.getSecurityContext().getUser().equals(user))) { resp.setStatus(HttpServletResponse.SC_FORBIDDEN); return; } ServiceLocator locator = serviceLocatorFactory.createServiceLocator(req); UserServiceResource resource = new UserServiceResource(user, locator); sendXmlResponse(resp, resource); return; } // /user/<username> UserResource resource = new UserResource(user, getUrlBase(req)); resp.setHeader("ETag", resource.getEntityTag()); sendXmlResponse(resp, resource); return; } // /server/status if (req.getPathInfo().equals("/server/status")) { processServerStatus(req, resp); return; } m = PATTERN_SPACE_USAGE.matcher(req.getPathInfo()); if (m.matches()) { String username = null; boolean isXml = false; boolean selected = false; if (m.group(1) != null && m.group(2) != null) { if (m.group(2).equals("/xml")) { // xml single user report username = m.group(1).substring(1); isXml = true; selected = true; } } else if (m.group(1) != null) { if (m.group(1).equals("/xml")) { // xml aggregate report username = null; isXml = true; selected = true; } else if (m.group(1).length() > 1) { // plaintext username report username = m.group(1).substring(1); isXml = false; selected = true; } } else { // plaintext aggregate report username = null; isXml = false; selected = true; } if (selected) { processSpaceUsage(req, resp, username, isXml); return; } } resp.setStatus(HttpServletResponse.SC_NOT_FOUND); } /** * Responds to the following operations: * * <ul> * <li><code>POST /server/gc</li> * </ul> * * Delegates all other operations to * {@link #doPut(HttpServletRequest, HttpServletResponse)}. */ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (req.getPathInfo().equals("/server/gc")) { processServerGc(req, resp); return; } if (req.getPathInfo().startsWith(URL_ACTIVATE)) { processActivateUser(req, resp); return; } if (req.getPathInfo().startsWith("/user/delete")) { processMultiUserDelete(req, resp); return; } if (req.getPathInfo().startsWith("/account/password/recover")) { processRecoverPassword(req, resp); return; } if (req.getPathInfo().startsWith("/account/activation/send")) { processSendActivation(req, resp); return; } if (req.getPathInfo().startsWith(URL_PASSWORD_RESET)) { processResetPassword(req, resp); return; } resp.setStatus(HttpServletResponse.SC_NOT_FOUND); } /** * Responds to the following operations: * * <ul> * <li><code>PUT /signup</code></li> * <li><code>PUT /account</code></li> * <li><code>PUT /user/&lgt;username></code></li> * </ul> */ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (!checkPutPreconditions(req, resp)) { return; } if (req.getPathInfo().equals("/signup")) { processSignup(req, resp); return; } if (req.getPathInfo().equals("/account")) { processAccountUpdate(req, resp); return; } if (req.getPathInfo().startsWith("/user/")) { String urlUsername = usernameFromPathInfo(req.getPathInfo()); if (urlUsername == null) { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } User user = userService.getUser(urlUsername); if (user != null) processUserUpdate(req, resp, user); else processUserCreate(req, resp); return; } resp.setStatus(HttpServletResponse.SC_NOT_FOUND); } // our methods /** */ public ContentService getContentService() { return contentService; } /** */ public void setContentService(ContentService contentService) { this.contentService = contentService; } /** */ public UserService getUserService() { return userService; } /** */ public void setUserService(UserService userService) { this.userService = userService; } /** */ public CosmoSecurityManager getSecurityManager() { return securityManager; } /** */ public void setSecurityManager(CosmoSecurityManager securityManager) { this.securityManager = securityManager; } /** */ public ServiceLocatorFactory getServiceLocatorFactory() { return serviceLocatorFactory; } /** */ public void setServiceLocatorFactory(ServiceLocatorFactory factory) { this.serviceLocatorFactory = factory; } public AccountActivator getAccountActivator() { return accountActivator; } public void setAccountActivator(AccountActivator activator) { this.accountActivator = activator; } public PasswordRecoverer getPasswordRecoverer() { return passwordRecoverer; } public void setPasswordRecoverer(PasswordRecoverer passwordRecoverer) { this.passwordRecoverer = passwordRecoverer; } public OutOfTheBoxHelper getOutOfTheBoxHelper() { return ootbHelper; } public void setOutOfTheBoxHelper(OutOfTheBoxHelper helper) { ootbHelper = helper; } public EntityFactory getEntityFactory() { return entityFactory; } public void setEntityFactory(EntityFactory entityFactory) { this.entityFactory = entityFactory; } // private methods /* Enforces preconditions on all PUT requests, including content * length and content type checks. Returns <code>true</code> if * all preconditions are met, otherwise sets the appropriate * error response code and returns <code>false</code>. */ private boolean checkPutPreconditions(HttpServletRequest req, HttpServletResponse resp) { if (req.getContentLength() <= 0) { resp.setStatus(HttpServletResponse.SC_LENGTH_REQUIRED); return false; } if (req.getContentType() == null || !req.getContentType().startsWith("text/xml")) { resp.setStatus(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); return false; } if (req.getHeader("Content-Transfer-Encoding") != null || req.getHeader("Content-Encoding") != null || req.getHeader("Content-Base") != null || req.getHeader("Content-Location") != null || req.getHeader("Content-MD5") != null || req.getHeader("Content-Range") != null) { resp.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED); return false; } return true; } /* * Delegated to by {@link #doDelete} to handle user DELETE * requests, removing the user and setting the response status and headers. */ private void processUserDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = usernameFromPathInfo(req.getPathInfo()); if (username == null) { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } if (username.equals(User.USERNAME_OVERLORD)) { resp.setStatus(HttpServletResponse.SC_FORBIDDEN); return; } userService.removeUser(username); resp.setStatus(HttpServletResponse.SC_NO_CONTENT); } /* Enforces preconditions on MultiUserDeletion. Returns * <code>true</code> if all preconditions are met, otherwise * sets the appropriate error response code and returns * <code>false</code>. */ private boolean checkMultiUserDeletePreconditions(HttpServletRequest req, HttpServletResponse resp) { if (req.getContentType() == null || !req.getContentType().startsWith("application/x-www-form-urlencoded")) { resp.setStatus(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); return false; } return true; } /* * Delegated to by {@link #doDelete} to handle * POST /user/delete requests. These request MUST have a message * body containing the names of the users to be deleted in standard * url-encoded form input syntax, ie, * * user=alice&user=bob&user=carlton */ private void processMultiUserDelete(HttpServletRequest req, HttpServletResponse resp) { if (checkMultiUserDeletePreconditions(req, resp)) { Set<String> names = new HashSet<String>(); for (String name : req.getParameterValues("user")) { names.add(name); } try { userService.removeUsersByName(names); } catch (OverlordDeletionException e) { resp.setStatus(HttpServletResponse.SC_FORBIDDEN); return; } resp.setStatus(HttpServletResponse.SC_NO_CONTENT); } } /* * Delegated to by {@link #doDelete} to handle account DELETE * requests, deleting the account for the currently logged in * user, setting the response status and headers, and writing the * response content. */ private void processAccountDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { User user = getLoggedInUser(); if (user.isOverlord()) { resp.setStatus(HttpServletResponse.SC_FORBIDDEN); return; } userService.removeUser(user); resp.setStatus(HttpServletResponse.SC_NO_CONTENT); } /* * Delegated to by {@link #doGet} to handle account GET * requests, retrieving the account for the currently logged in * user, setting the response status and headers, and writing the * response content. */ private void processAccountGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { User user = getLoggedInUser(); UserResource resource = new UserResource(user, getUrlBase(req)); resp.setStatus(HttpServletResponse.SC_OK); resp.setHeader("ETag", resource.getEntityTag()); sendXmlResponse(resp, resource); } /* * Turn a paging query string into a PageCriteria object. */ private PageCriteria<User.SortType> buildPageCriteria(HttpServletRequest req) { PageCriteria<User.SortType> pageCriteria = new PageCriteria<User.SortType>(DEFAULT_PAGE_NUMBER, DEFAULT_PAGE_SIZE, DEFAULT_SORT_ASCENDING, DEFAULT_SORT_TYPE); Map<String, String[]> pagingParameterMap = req.getParameterMap(); try { if (pagingParameterMap.containsKey(PageCriteria.PAGE_SIZE_URL_KEY)) { int pageSize = Integer.parseInt(pagingParameterMap.get(PageCriteria.PAGE_SIZE_URL_KEY)[0]); if (pageSize == 0) { throw new CmpException("Page size cannot be 0."); } pageCriteria.setPageSize(pageSize); } } catch (NumberFormatException e) { throw new CmpException( pagingParameterMap.get(PageCriteria.PAGE_SIZE_URL_KEY)[0] + " is not a valid page size."); } try { if (pagingParameterMap.containsKey(PageCriteria.PAGE_NUMBER_URL_KEY)) { pageCriteria.setPageNumber( Integer.parseInt(pagingParameterMap.get(PageCriteria.PAGE_NUMBER_URL_KEY)[0])); } } catch (NumberFormatException e) { throw new CmpException( pagingParameterMap.get(PageCriteria.PAGE_NUMBER_URL_KEY)[0] + " is not a valid page number."); } if (pagingParameterMap.containsKey(PageCriteria.SORT_ORDER_URL_KEY)) { String sortOrderParameter = pagingParameterMap.get(PageCriteria.SORT_ORDER_URL_KEY)[0]; if (sortOrderParameter.equals(PageCriteria.ASCENDING_STRING)) { pageCriteria.setSortAscending(true); } else if (sortOrderParameter.equals(PageCriteria.DESCENDING_STRING)) { pageCriteria.setSortAscending(false); } else { throw new CmpException("Sort order " + sortOrderParameter + " not valid."); } } if (pagingParameterMap.containsKey(PageCriteria.SORT_TYPE_URL_KEY)) { User.SortType sortType = User.SortType .getByUrlString(pagingParameterMap.get(PageCriteria.SORT_TYPE_URL_KEY)[0]); if (sortType != null) { pageCriteria.setSortType(sortType); } else { throw new CmpException( "Sort type " + pagingParameterMap.get(PageCriteria.SORT_TYPE_URL_KEY)[0] + " not valid."); } } if (pagingParameterMap.containsKey(PageCriteria.QUERY_URL_KEY)) { String searchTerm = pagingParameterMap.get(PageCriteria.QUERY_URL_KEY)[0]; pageCriteria.addOr("firstName", searchTerm); pageCriteria.addOr("lastName", searchTerm); pageCriteria.addOr("email", searchTerm); pageCriteria.addOr("username", searchTerm); } return pageCriteria; } /* * Delegated to by {@link #doGet} to handle requests * for the total number of users in the system. */ private void processGetUserCount(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { resp.setStatus(HttpServletResponse.SC_OK); sendPlainTextResponse(resp, Integer.toString(getUserList(req, resp).size())); } catch (CmpException e) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); } } /* * Delegated to by {@link #doGet} to handle users GET * requests, retrieving all user accounts, setting the response * status and headers, and writing the response content. */ private void processUsersGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { resp.setStatus(HttpServletResponse.SC_OK); sendXmlResponse(resp, new UsersResource(getUserList(req, resp), getUrlBase(req), req.getParameterMap())); } catch (CmpException e) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); } } private Collection<User> getUserList(HttpServletRequest req, HttpServletResponse resp) throws CmpException { Collection<User> users; if (req.getQueryString() != null) { users = userService.getUsers(buildPageCriteria(req)).getList(); } else { users = userService.getUsers(); } return users; } /* * Delegated to by {@link #doGet} to handle server status GET * requests, taking a status snapshot, setting the response * status and headers, and writing the response content. */ private void processServerStatus(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setStatus(HttpServletResponse.SC_OK); StatusSnapshotResource resource = new StatusSnapshotResource(new StatusSnapshot()); sendPlainTextResponse(resp, resource); } /* * Delegated to by {@link #doGet} to handle space usage requests. */ private void processSpaceUsage(HttpServletRequest req, HttpServletResponse resp, String username, boolean isXml) throws ServletException, IOException { User user = null; if (username != null) { user = userService.getUser(username); if (user == null) { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } if (user.isOverlord()) { resp.setStatus(HttpServletResponse.SC_FORBIDDEN); return; } } SpaceUsageResource resource = null; if (user == null) { if (log.isDebugEnabled()) log.debug("generating usage report for all users"); HashSet<SpaceUsageReport> reports = new HashSet<SpaceUsageReport>(); for (User u : userService.getUsers()) { if (u.isOverlord()) continue; HomeCollectionItem home = contentService.getRootItem(u); SpaceUsageReport report = new SpaceUsageReport(u, home); reports.add(report); } resource = new SpaceUsageResource(reports); } else { if (log.isDebugEnabled()) log.debug("generating usage report for user " + username); HomeCollectionItem home = contentService.getRootItem(user); resource = new SpaceUsageResource(new SpaceUsageReport(user, home)); } resp.setStatus(HttpServletResponse.SC_OK); if (isXml) sendXmlResponse(resp, resource); else sendPlainTextResponse(resp, resource); } /* * Delegated to by {@link #doPost} to handle server gc POST * requests, initiating garbage collection, and setting the * response status. */ private void processServerGc(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.gc(); resp.setStatus(HttpServletResponse.SC_NO_CONTENT); } /* * Delegated to by {@link #doPost} to handle user activation * requests. */ private void processActivateUser(HttpServletRequest req, HttpServletResponse resp) { String username = req.getPathInfo().substring(URL_ACTIVATE.length()); try { User user = userService.getUser(username); if (user.isActivated()) { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } user.activate(); userService.updateUser(user); resp.setStatus(HttpServletResponse.SC_NO_CONTENT); } catch (DataRetrievalFailureException e) { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } } /* * Delegated to by {@link #doPut} to handle signup * requests, creating the user account and setting the response * status and headers. */ private void processSignup(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (disableSignups) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); } else { try { Document xmldoc = readXmlRequest(req); UserResource resource = new UserResource(getUrlBase(req), xmldoc, entityFactory); User user = resource.getUser(); user.setAdmin(Boolean.FALSE); user.setLocked(Boolean.FALSE); user = userService.createUser(user, createSignupListeners(req)); resource = new UserResource(user, getUrlBase(req)); resp.setStatus(HttpServletResponse.SC_CREATED); resp.setHeader("Content-Location", resource.getHomedirUrl()); resp.setHeader("ETag", resource.getEntityTag()); sendXmlResponse(resp, resource); } catch (SAXException e) { log.warn("error parsing request body: " + e.getMessage()); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error parsing request body: " + e.getMessage()); return; } catch (CmpException e) { log.warn("bad request for signup: " + e.getMessage()); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); } catch (ModelValidationException e) { handleModelValidationError(resp, e); } catch (InvalidStateException ise) { handleInvalidStateException(resp, ise); } } } /* * Delegated to by {@link #doPut} to handle account update * requests for the currently logged in user, saving the modified * account and setting the response status and headers. */ private void processAccountUpdate(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { Document xmldoc = readXmlRequest(req); String urlUsername = usernameFromPathInfo(req.getPathInfo()); User user = getLoggedInUser(); String oldUsername = user.getUsername(); Boolean oldAdmin = user.getAdmin(); Boolean oldLocked = user.isLocked(); UserResource resource = new UserResource(user, getUrlBase(req), xmldoc, entityFactory); if (user.isUsernameChanged()) { // reset logged in user's username user.setUsername(oldUsername); log.warn("bad request for account update: " + "username may not be changed"); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Username may not be changed"); return; } if (user.isAdminChanged() && !oldAdmin) { // Non admin tried to change admin status user.setAdmin(oldAdmin); log.warn("bad request for account update: " + "non-admin may not change own admin status"); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Non admin may not change own admin status."); return; } if (user.isLocked() != oldLocked) { user.setLocked(oldLocked); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "User may not changed own locked status."); } userService.updateUser(user); resp.setStatus(HttpServletResponse.SC_NO_CONTENT); resp.setHeader("ETag", resource.getEntityTag()); } catch (SAXException e) { log.warn("error parsing request body: " + e.getMessage()); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error parsing request body: " + e.getMessage()); return; } catch (CmpException e) { log.warn("bad request for account update: " + e.getMessage()); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); } catch (ModelValidationException e) { handleModelValidationError(resp, e); } catch (InvalidStateException ise) { handleInvalidStateException(resp, ise); } } /* * Delegated to by {@link #doPut} to handle account creation * requests, creating the user account and setting the response * status and headers. */ private void processUserCreate(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { Document xmldoc = readXmlRequest(req); String urlUsername = usernameFromPathInfo(req.getPathInfo()); UserResource resource = new UserResource(getUrlBase(req), xmldoc, entityFactory); User user = resource.getUser(); if (user.getUsername() != null && !user.getUsername().equals(urlUsername)) { log.warn("bad request for user create: " + "username does not match request URI"); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Username does not match request URI"); return; } user = userService.createUser(user, createUserCreateListeners(req)); resp.setStatus(HttpServletResponse.SC_CREATED); resp.setHeader("ETag", resource.getEntityTag()); } catch (SAXException e) { log.warn("error parsing request body: " + e.getMessage()); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error parsing request body: " + e.getMessage()); return; } catch (CmpException e) { log.warn("bad request for user create: " + e.getMessage()); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); } catch (ModelValidationException e) { handleModelValidationError(resp, e); } catch (InvalidStateException ise) { handleInvalidStateException(resp, ise); } } /* * Delegated to by {@link #doPut} to handle account update * requests, saving the modified account and setting the response * status and headers. */ private void processUserUpdate(HttpServletRequest req, HttpServletResponse resp, User user) throws ServletException, IOException { try { Document xmldoc = readXmlRequest(req); String urlUsername = usernameFromPathInfo(req.getPathInfo()); UserResource resource = new UserResource(user, getUrlBase(req), xmldoc, entityFactory); userService.updateUser(user); resp.setStatus(HttpServletResponse.SC_NO_CONTENT); resp.setHeader("ETag", resource.getEntityTag()); if (!user.getUsername().equals(urlUsername)) { resp.setHeader("Content-Location", resource.getUserUrl()); } } catch (SAXException e) { log.warn("error parsing request body: " + e.getMessage()); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error parsing request body: " + e.getMessage()); return; } catch (CmpException e) { log.warn("bad request for user update: " + e.getMessage()); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); } catch (ModelValidationException e) { handleModelValidationError(resp, e); } catch (InvalidStateException ise) { handleInvalidStateException(resp, ise); } } /* * Delegated to by {@link #doPost} to handle password * recovery requests. * */ private void processRecoverPassword(final HttpServletRequest req, final HttpServletResponse resp) throws IOException, ServletException { User user = getUserFromParameter(req); if (user != null) { String passwordRecoveryKey = passwordRecoverer.createRecoveryKey(); final PasswordRecovery passwordRecovery = entityFactory.createPasswordRecovery(user, passwordRecoveryKey); userService.createPasswordRecovery(passwordRecovery); PasswordRecoveryMessageContext context = createPasswordRecoveryMessageContext(req, passwordRecovery); passwordRecoverer.sendRecovery(passwordRecovery, context); resp.setStatus(HttpServletResponse.SC_NO_CONTENT); } else { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); } } private void processResetPassword(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path = req.getPathInfo(); String passwordResetKey = path.substring(URL_PASSWORD_RESET.length()); PasswordRecovery passwordRecovery = userService.getPasswordRecovery(passwordResetKey); if (passwordRecovery != null) { String newPassword = req.getParameter("password"); if (newPassword != null) { User user = passwordRecovery.getUser(); user.setPassword(newPassword); try { userService.updateUser(user); } catch (ModelValidationException e) { // can happen when password is bad handleModelValidationError(resp, e); return; } userService.deletePasswordRecovery(passwordRecovery); resp.setStatus(HttpServletResponse.SC_NO_CONTENT); } else { resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); } } else { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); } } /* * Delegated to by {@link #doPost} to handle activation email * resend requests * */ private void processSendActivation(final HttpServletRequest req, final HttpServletResponse resp) throws IOException, ServletException { User user = getUserFromParameter(req); if (user != null && user.getActivationId() != null) { accountActivator.sendActivationMessage(user, createActivationContext(req)); resp.setStatus(HttpServletResponse.SC_NO_CONTENT); } else { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); } } private User getUserFromParameter(HttpServletRequest req) { String username = req.getParameter("username"); String email = req.getParameter("email"); if (username != null) { return this.userService.getUser(username); } else if (email != null) { return this.userService.getUserByEmail(email); } else return null; } private void handleInvalidStateException(HttpServletResponse resp, InvalidStateException ise) throws IOException { String message = ise.getInvalidValues()[0].getMessage(); log.warn("model validation error: " + message); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, message); } private void handleModelValidationError(HttpServletResponse resp, ModelValidationException e) throws IOException { if (e instanceof DuplicateUsernameException) { sendApiError(resp, CmpConstants.SC_USERNAME_IN_USE); return; } if (e instanceof DuplicateEmailException) { sendApiError(resp, CmpConstants.SC_EMAIL_IN_USE); return; } log.warn("model validation error: " + e.getMessage()); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); } private void sendApiError(HttpServletResponse resp, int errorCode) throws IOException { resp.sendError(errorCode, CmpConstants.getReasonPhrase(errorCode)); } private Object getBean(String name, Class clazz) throws ServletException { try { return wac.getBean(name, clazz); } catch (BeansException e) { throw new ServletException( "Error retrieving bean " + name + " of type " + clazz + " from web application context", e); } } private User getLoggedInUser() { return securityManager.getSecurityContext().getUser(); } private String usernameFromPathInfo(String pathInfo) { if (pathInfo.startsWith("/user/")) { String username = pathInfo.substring(6); // Find the end of the username int endIndex = username.indexOf("/"); if (endIndex > -1) { username = username.substring(0, endIndex); } if (!(username.equals("") || username.indexOf("/") >= 0)) { return username; } } return null; } private Document readXmlRequest(HttpServletRequest req) throws SAXException, IOException { if (req.getContentLength() == 0) { return null; } InputStream in = req.getInputStream(); if (in == null) { return null; } // check to see if there's any data to read PushbackInputStream filtered = new PushbackInputStream(in, 1); int read = filtered.read(); if (read == -1) { return null; } filtered.unread(read); // there is data, so read the stream try { BUILDER_FACTORY.setNamespaceAware(true); DocumentBuilder docBuilder = BUILDER_FACTORY.newDocumentBuilder(); return docBuilder.parse(filtered); } catch (ParserConfigurationException e) { throw new CmpException("error configuring xml builder", e); } } private void sendXmlResponse(HttpServletResponse resp, OutputsXml resource) throws ServletException, IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); try { Document doc = BUILDER_FACTORY.newDocumentBuilder().newDocument(); doc.appendChild(resource.toXml(doc)); OutputFormat format = new OutputFormat("xml", "UTF-8", true); XMLSerializer serializer = new XMLSerializer(out, format); serializer.setNamespaces(true); serializer.asDOMSerializer().serialize(doc); } catch (ParserConfigurationException e) { throw new CmpException("error configuring xml builder", e); } byte[] bytes = out.toByteArray(); resp.setContentType(CmpConstants.MEDIA_TYPE_XML); resp.setCharacterEncoding("UTF-8"); resp.setContentLength(bytes.length); resp.getOutputStream().write(bytes); } private void sendPlainTextResponse(HttpServletResponse resp, String text) throws ServletException, IOException { resp.setContentType(CmpConstants.MEDIA_TYPE_PLAIN_TEXT); resp.setCharacterEncoding("UTF-8"); resp.setContentLength(text.length()); resp.getWriter().write(text); } private void sendPlainTextResponse(HttpServletResponse resp, OutputsPlainText resource) throws ServletException, IOException { sendPlainTextResponse(resp, resource.toText()); } private String getUrlBase(HttpServletRequest req) { // like response.encodeUrl() except does not include servlet // path or session id StringBuffer buf = new StringBuffer(); buf.append(req.getScheme()).append("://").append(req.getServerName()); if ((req.getScheme().equals("https") && req.getServerPort() != 443) || (req.getScheme().equals("http") && req.getServerPort() != 80)) { buf.append(":").append(req.getServerPort()); } if (!req.getContextPath().equals("/")) { buf.append(req.getContextPath()); } return buf.toString(); } private ServiceListener[] createSignupListeners(HttpServletRequest req) { if (this.createOutOfBoxCollections) return new ServiceListener[] { createActivationListener(req), createOotbListener(req) }; else return new ServiceListener[] { createActivationListener(req) }; } private ServiceListener[] createUserCreateListeners(HttpServletRequest req) { if (this.createOutOfBoxCollections) return new ServiceListener[] { createOotbListener(req) }; else return new ServiceListener[] {}; } private ActivationListener createActivationListener(HttpServletRequest req) { return new ActivationListener(accountActivator, createActivationContext(req)); } private ActivationContext createActivationContext(HttpServletRequest req) { ActivationContext activationContext = new ActivationContext(); String urlBase = getUrlBase(req); activationContext.setLocale(req.getLocale()); activationContext.setActivationLinkTemplate( urlBase + "/account/activate/" + "{" + ActivationContext.LINK_TEMPLATE_VAR_ACTIVATION_ID + "}"); activationContext.setHostname(urlBase); activationContext.setSender(userService.getUser(User.USERNAME_OVERLORD)); return activationContext; } private OutOfTheBoxListener createOotbListener(HttpServletRequest req) { OutOfTheBoxContext context = new OutOfTheBoxContext(); context.setLocale(req.getLocale()); // XXX get timezone from user somehow context.setTimeZone(TimeZone.getDefault()); return new OutOfTheBoxListener(ootbHelper, context); } private PasswordRecoveryMessageContext createPasswordRecoveryMessageContext(HttpServletRequest req, PasswordRecovery recovery) { PasswordRecoveryMessageContext context = new PasswordRecoveryMessageContext(); String urlBase = getUrlBase(req); context.setSender(userService.getUser(User.USERNAME_OVERLORD)); context.setLocale(req.getLocale()); context.setRecoveryLink(urlBase + URL_PASSWORD_RESET + recovery.getKey()); context.setHostname(urlBase); return context; } public boolean isDisableSignups() { return disableSignups; } public void setDisableSignups(boolean disableSignups) { this.disableSignups = disableSignups; } }