/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * licenses this file to you 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 the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>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.apereo.portal.portlets.tenantmanager; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Resource; import javax.portlet.ActionRequest; import javax.portlet.ActionResponse; import javax.portlet.PortletRequest; import javax.portlet.PortletSession; import javax.portlet.RenderRequest; import org.apache.commons.lang.StringUtils; import org.apereo.portal.tenants.ITenant; import org.apereo.portal.tenants.ITenantManagementAction; import org.apereo.portal.tenants.ITenantOperationsListener; import org.apereo.portal.tenants.TenantOperationResponse; import org.apereo.portal.tenants.TenantService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.portlet.bind.annotation.ActionMapping; import org.springframework.web.portlet.bind.annotation.RenderMapping; import org.springframework.web.servlet.ModelAndView; @Controller @RequestMapping(value = "VIEW") public class TenantManagerController { private static final String DEFAULT_VIEW_NAME = "/jsp/TenantManager/tenantManager"; private static final String TENANT_DETAILS_VIEW_NAME = "/jsp/TenantManager/tenantDetails"; private static final String ADD_TENANT_VIEW_NAME = "/jsp/TenantManager/addTenant"; private static final String REPORT_VIEW_NAME = "/jsp/TenantManager/report"; private static final String TENANT_MANAGER_ATTRIBUTES = "tenantManagerAttributes"; private static final String OPTIONAL_OPERATIONS_LISTENERS = "optionalOperationsListeners"; private static final String OPTIONAL_LISTENER_PARAMETER = "optionalListener"; private static final String OPERATION_NAME_CODE = "operationNameCode"; private static final String OPERATIONS_LISTENER_RESPONSES = "operationsListenerResponses"; private static final String OPERATIONS_LINTENER_AVAILABLE_ACTIONS = "operationsListenerAvailableActions"; private static final String INVALID_FIELDS = "invalidFields"; private static final String PREVIOUS_RESPONSES = "previousResponses"; private static final String CURRENT_TENANT_SESSION_ATTRIBUTE = TenantManagerController.class.getName() + ".currentTenant"; /** * Handy collection of things that might be stored in the {@link PortletSession} for easy * cleanup. */ private static final String[] SESSION_KEYS = new String[] { CURRENT_TENANT_SESSION_ATTRIBUTE, INVALID_FIELDS, PREVIOUS_RESPONSES, OPERATION_NAME_CODE, OPERATIONS_LISTENER_RESPONSES }; @Autowired private TenantService tenantService; @Resource(name = "tenantManagerAttributes") private Map<String, String> tenantManagerAttributes; private final Logger log = LoggerFactory.getLogger(getClass()); @RenderMapping public ModelAndView showDefault(final RenderRequest req) { // First reset any workflows the user may have undertaken clearState(req); final Map<String, Object> model = new HashMap<String, Object>(); final List<ITenant> tenantsList = tenantService.getTenantsList(); model.put("tenantsList", tenantsList); return new ModelAndView(DEFAULT_VIEW_NAME, model); } @RenderMapping(params = "action=showAddTenant") public ModelAndView showAddTenant(final PortletSession session) { Map<String, Object> model = new HashMap<String, Object>(); model.put(TENANT_MANAGER_ATTRIBUTES, Collections.unmodifiableMap(tenantManagerAttributes)); model.put(OPTIONAL_OPERATIONS_LISTENERS, tenantService.getOptionalOperationsListeners()); /* * The following 2 items are empty the first time you visit the screen, * but may contain data if you attempted to create a tenant but your * inputs failed validation. */ Map<String, String> previousResponses = Collections.emptyMap(); // default if (session.getAttributeMap().containsKey(PREVIOUS_RESPONSES)) { previousResponses = (Map<String, String>) session.getAttribute(PREVIOUS_RESPONSES); } model.put(PREVIOUS_RESPONSES, previousResponses); Map<String, Object> invalidFields = Collections.emptyMap(); // default if (session.getAttributeMap().containsKey(INVALID_FIELDS)) { invalidFields = (Map<String, Object>) session.getAttribute(INVALID_FIELDS); } model.put(INVALID_FIELDS, invalidFields); return new ModelAndView(ADD_TENANT_VIEW_NAME, model); } /** @since 4.3 */ @RenderMapping(params = "action=showTenantDetails") public ModelAndView showTenantDetails(final RenderRequest req, final PortletSession session) { // Should the user chose to perform an action on the details screen, we // will need to know which tenant upon which to invoke it. Not crazy // about storing this object in the PortletSession, but taking it as a // @RequestParameter could increase the challenges of URL-hacking // diligence down the road. Every pass through this method will re-set // the tenancy under the microscope. ITenant tenant = null; // There are two possibilities that work... final String fnameParameter = req.getParameter("fname"); if (!StringUtils.isBlank(fnameParameter)) { // An fname came in the request; this possibility trumps others tenant = tenantService.getTenantByFName(fnameParameter); session.setAttribute(CURRENT_TENANT_SESSION_ATTRIBUTE, tenant); } else if (session.getAttributeMap().containsKey(CURRENT_TENANT_SESSION_ATTRIBUTE)) { // A tenant was previously identified; we are most likely // re-playing the tenant details after failed validation tenant = (ITenant) session.getAttribute(CURRENT_TENANT_SESSION_ATTRIBUTE); } Map<String, Object> model = new HashMap<String, Object>(); model.put("tenant", tenant); model.put("tenantManagerAttributes", Collections.unmodifiableMap(tenantManagerAttributes)); model.put(OPERATIONS_LINTENER_AVAILABLE_ACTIONS, tenantService.getAllAvaialableActions()); /* * The following 2 items are empty the first time you visit the screen, * but may contain data if you attempted to create a tenant but your * inputs failed validation. */ Map<String, String> previousResponses = Collections.emptyMap(); // default if (session.getAttributeMap().containsKey(PREVIOUS_RESPONSES)) { previousResponses = (Map<String, String>) session.getAttribute(PREVIOUS_RESPONSES); } model.put(PREVIOUS_RESPONSES, previousResponses); Map<String, Object> invalidFields = Collections.emptyMap(); // default if (session.getAttributeMap().containsKey(INVALID_FIELDS)) { invalidFields = (Map<String, Object>) session.getAttribute(INVALID_FIELDS); } model.put(INVALID_FIELDS, invalidFields); return new ModelAndView(TENANT_DETAILS_VIEW_NAME, model); } /** @since 4.3 */ @RenderMapping(params = "action=showReport") public ModelAndView showReport(final RenderRequest req) { Map<String, Object> model = new HashMap<String, Object>(); PortletSession session = req.getPortletSession(); model.put(OPERATION_NAME_CODE, session.getAttribute(OPERATION_NAME_CODE)); model.put( OPERATIONS_LISTENER_RESPONSES, session.getAttribute(OPERATIONS_LISTENER_RESPONSES)); return new ModelAndView(REPORT_VIEW_NAME, model); } @ActionMapping(params = "action=doAddTenant") public void doAddTenant( ActionRequest req, ActionResponse res, final PortletSession session, @RequestParam("name") String name) { final Map<String, String> attributes = gatherAttributesFromPortletRequest(req); final String fname = calculateFnameFromName(name); // Validation final Set<String> invalidFields = detectInvalidFields(name, fname, attributes); if (!invalidFields.isEmpty()) { /* * Something wasn't valid; return the user to the addTenant screen. */ this.returnToInvalidForm(req, res, name, attributes, invalidFields, "showAddTenant"); return; } // Honor the user's choices as far as optional listeners final List<String> selectedListenerFnames = (req.getParameterValues(OPTIONAL_LISTENER_PARAMETER) != null) ? Arrays.asList(req.getParameterValues(OPTIONAL_LISTENER_PARAMETER)) : new ArrayList<String>(0); // None were selected final Set<String> skipListenerFnames = new HashSet<>(); for (ITenantOperationsListener listener : tenantService.getOptionalOperationsListeners()) { if (!selectedListenerFnames.contains(listener.getFname())) { skipListenerFnames.add(listener.getFname()); } } final List<TenantOperationResponse> responses = new ArrayList<>(); tenantService.createTenant(name, fname, attributes, skipListenerFnames, responses); forwardToReportScreen(req, res, "tenant.manager.add", responses); } @ActionMapping(params = "action=doUpdateTenant") public void doUpdateTenant( final ActionRequest req, final ActionResponse res, final PortletSession session) { final ITenant tenant = (ITenant) session.getAttribute(CURRENT_TENANT_SESSION_ATTRIBUTE); if (tenant == null) { throw new IllegalStateException("No current tenant"); } final Map<String, String> attributes = gatherAttributesFromPortletRequest(req); // Validation final Set<String> invalidFields = detectInvalidFields(tenant.getName(), tenant.getFname(), attributes); if (!invalidFields.isEmpty()) { /* * Something wasn't valid; return the user to the addTenant screen. */ this.returnToInvalidForm( req, res, tenant.getName(), attributes, invalidFields, "showTenantDetails"); return; } final List<TenantOperationResponse> responses = new ArrayList<>(); tenantService.updateTenant(tenant, attributes, responses); forwardToReportScreen(req, res, "tenant.manager.update.attributes", responses); } @ActionMapping(params = "action=doRemoveTenant") public void doRemoveTenant( ActionRequest req, ActionResponse res, final PortletSession session, @RequestParam("fname") String fname) { List<TenantOperationResponse> responses = new ArrayList<>(); tenantService.deleteTenantByFName(fname, responses); forwardToReportScreen(req, res, "tenant.manager.remove.tenant", responses); } /** @since 4.3 */ @ActionMapping(params = "action=doListenerAction") public void doListenerAction( ActionRequest req, ActionResponse res, @RequestParam("fname") String fname, final PortletSession session) { final ITenantManagementAction action = tenantService.getAction(fname); final ITenant tenant = (ITenant) session.getAttribute(CURRENT_TENANT_SESSION_ATTRIBUTE); if (tenant == null) { throw new IllegalStateException("No current tenant"); } TenantOperationResponse response = action.invoke(tenant); forwardToReportScreen( req, res, action.getMessageCode(), Collections.singletonList(response)); } /* * Implementation */ private void returnToInvalidForm( final ActionRequest req, final ActionResponse res, final String name, final Map<String, String> attributes, final Set<String> invalidFields, final String actionParameter) { /* * JSP/JSTL/EL is not good at collection.contains(); Convert the * invalidFields to a format that's easy to read in the JSP. */ final Map<String, Object> invalidFieldsMap = new HashMap<>(); for (String fieldName : invalidFields) { invalidFieldsMap.put(fieldName, Boolean.TRUE); } // Need to store some items to display invalid fields; would be // handy to have support for javax.portlet.actionScopedRequestAttributes final PortletSession session = req.getPortletSession(); session.setAttribute(INVALID_FIELDS, invalidFieldsMap); final Map<String, String> previousResponses = new HashMap<>(); previousResponses.put("name", name); previousResponses.putAll(attributes); session.setAttribute(PREVIOUS_RESPONSES, previousResponses); // Send the user to the report screen res.setRenderParameter("action", actionParameter); } private void forwardToReportScreen( final ActionRequest req, final ActionResponse res, final String operationNameCode, final List<TenantOperationResponse> responses) { final PortletSession session = req.getPortletSession(); // Need to store some items to share with user in the report; would be // handy to have support for javax.portlet.actionScopedRequestAttributes session.setAttribute(OPERATION_NAME_CODE, operationNameCode); session.setAttribute(OPERATIONS_LISTENER_RESPONSES, responses); // Send the user to the report screen res.setRenderParameter("action", "showReport"); } /** Returns a collection of invalid fields, if any. */ private Set<String> detectInvalidFields( final String name, final String fname, final Map<String, String> attributes) { final Set<String> rslt = new HashSet<>(); // Name & Fname try { tenantService.validateName(name); // Fname is generated from name; the only way to // fix an invalid fname is to change the name. tenantService.validateFname(fname); } catch (Exception e) { log.warn("Validation failure for tenant name={}", name, e); rslt.add("name"); } // Attributes for (String attributeName : tenantManagerAttributes.keySet()) { try { final String value = attributes.get(attributeName); tenantService.validateAttribute(attributeName, value); } catch (Exception e) { log.warn("Validation failure for tenant name={}", name, e); rslt.add(attributeName); } } return rslt; } private String calculateFnameFromName(final String name) { return name.replaceAll("[\\s']", "_").toLowerCase(); } private Map<String, String> gatherAttributesFromPortletRequest(ActionRequest req) { Map<String, String> rslt = new HashMap<String, String>(); for (Map.Entry<String, String> y : tenantManagerAttributes.entrySet()) { final String key = y.getKey(); final String value = req.getParameter(key); if (StringUtils.isNotBlank(value)) { rslt.put(key, value); } } return rslt; } private void clearState(final PortletRequest req) { final PortletSession session = req.getPortletSession(); for (String key : SESSION_KEYS) { session.removeAttribute(key); } } }