/**
* 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.tenants;
import java.util.ArrayList;
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 java.util.regex.Pattern;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apereo.portal.events.IPortalTenantEventFactory;
import org.apereo.portal.url.IPortalRequestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* Defines the contract for Tenant operations vis-a-vis other subsystems.
*
* @since 4.1
*/
@Service
public class TenantService {
private static final String TENANT_NAME_VALIDATOR_REGEX = "^[\\w ]{5,32}$";
private static final Pattern TENANT_NAME_VALIDATOR_PATTERN =
Pattern.compile(TENANT_NAME_VALIDATOR_REGEX);
private static final String TENANT_FNAME_VALIDATOR_REGEX = "^[a-z_0-9]{5,32}$";
private static final Pattern TENANT_FNAME_VALIDATOR_PATTERN =
Pattern.compile(TENANT_FNAME_VALIDATOR_REGEX);
private final Logger log = LoggerFactory.getLogger(getClass());
@Autowired private ITenantDao tenantDao;
@Resource(name = "tenantOperationsListeners")
private List<ITenantOperationsListener> tenantOperationsListeners;
private List<ITenantOperationsListener> optionalOperationsListeners;
private Map<String, ITenantManagementAction> operationsListenerAvailableActions;
@Autowired private IPortalRequestUtils portalRequestUtils;
@Autowired private IPortalTenantEventFactory tenantEventFactory;
/** @since 4.3 */
@PostConstruct
public void init() {
Map<String, ITenantManagementAction> map = new HashMap<>();
List<ITenantOperationsListener> optional = new ArrayList<>();
for (ITenantOperationsListener listener : tenantOperationsListeners) {
if (listener.isOptional()) {
optional.add(listener);
}
for (ITenantManagementAction action : listener.getAvaialableActions()) {
map.put(action.getFname(), action);
}
}
optionalOperationsListeners = Collections.unmodifiableList(optional);
operationsListenerAvailableActions = Collections.unmodifiableMap(map);
}
/** @since 4.3 */
public ITenant getTenantByFName(final String fname) {
// Assertions
if (StringUtils.isBlank(fname)) {
String msg = "Argument 'fname' cannot be blank";
throw new IllegalArgumentException(msg);
}
return tenantDao.getTenantByFName(fname);
}
/**
* Provides the complete collection of tenants in the system in the default order
* (alphabetically by name).
*/
public List<ITenant> getTenantsList() {
List<ITenant> rslt = new ArrayList<ITenant>(tenantDao.getAllTenants());
Collections.sort(rslt);
return rslt;
}
public ITenant createTenant(
final String name,
final String fname,
final Map<String, String> attributes,
final Set<String> skipListenerFnames,
final List<TenantOperationResponse> responses) {
/*
* NB: Ideally this method should be annotated with @PortalTransactional,
* but (unfortunately) it doesn't work. There are multiple approaches to
* persistence (JPA and pre-JPA) at play in the concrete
* ITenantOperationsListener objects.
*/
// Input validation
Validate.validState(
TENANT_NAME_VALIDATOR_PATTERN.matcher(name).matches(),
"Invalid tenant name '%s' -- names must match %s .",
name,
TENANT_NAME_VALIDATOR_REGEX);
Validate.validState(
TENANT_FNAME_VALIDATOR_PATTERN.matcher(fname).matches(),
"Invalid tenant fname '%s' -- fnames must match %s .",
fname,
TENANT_FNAME_VALIDATOR_REGEX);
// Create the concrete tenant object
final ITenant rslt = tenantDao.instantiate();
rslt.setName(name);
rslt.setFname(fname);
for (Map.Entry<String, String> y : attributes.entrySet()) {
rslt.setAttribute(y.getKey(), y.getValue());
}
log.info("Creating new tenant: {}", rslt.toString());
// Invoke the listeners
for (ITenantOperationsListener listener : this.tenantOperationsListeners) {
// Skip listeners as requested
if (skipListenerFnames != null
&& listener.isOptional()
&& skipListenerFnames.contains(listener.getFname())) {
continue;
}
TenantOperationResponse res = null; // default
try {
res = listener.onCreate(rslt);
if (!TenantOperationResponse.Result.IGNORE.equals(res.getResult())) {
responses.add(res);
}
} catch (Exception e) {
final String msg =
"Error invoking ITenantOperationsListener '"
+ listener.toString()
+ "' for tenant: "
+ rslt.toString();
throw new RuntimeException(msg, e);
}
if (res.getResult().equals(TenantOperationResponse.Result.ABORT)) {
log.warn(
"ITenantOperationsListener {} aborted the creation of tenant: ",
listener.toString(),
rslt.toString());
// TODO: Can we rollback somehow?
break;
}
}
// Fire an appropriate PortalEvent
final HttpServletRequest request = portalRequestUtils.getCurrentPortalRequest();
tenantEventFactory.publishTenantCreatedTenantEvent(request, this, rslt);
return rslt;
}
public ITenant updateTenant(
final ITenant tenant,
final Map<String, String> attributes,
final List<TenantOperationResponse> responses) {
/*
* NB: Ideally this method should be annotated with @PortalTransactional,
* but (unfortunately) it doesn't work. There are multiple approaches to
* persistence (JPA and pre-JPA) at play in the concrete
* ITenantOperationsListener objects.
*/
for (Map.Entry<String, String> y : attributes.entrySet()) {
tenant.setAttribute(y.getKey(), y.getValue());
}
log.info("Updating tenant: {}", tenant.toString());
// Invoke the listeners
for (ITenantOperationsListener listener : this.tenantOperationsListeners) {
TenantOperationResponse res = null; // default
try {
res = listener.onUpdate(tenant);
if (!TenantOperationResponse.Result.IGNORE.equals(res.getResult())) {
responses.add(res);
}
} catch (Exception e) {
final String msg =
"Error invoking ITenantOperationsListener '"
+ listener.toString()
+ "' for tenant: "
+ tenant.toString();
throw new RuntimeException(msg, e);
}
if (res.getResult().equals(TenantOperationResponse.Result.ABORT)) {
log.warn(
"ITenantOperationsListener {} aborted updating tenant: ",
listener.toString(),
tenant.toString());
// TODO: Can we rollback somehow?
break;
}
}
// Fire an appropriate PortalEvent
final HttpServletRequest request = portalRequestUtils.getCurrentPortalRequest();
tenantEventFactory.publishTenantUpdatedTenantEvent(request, this, tenant);
return tenant;
}
public void deleteTenantByFName(String fname, List<TenantOperationResponse> responses) {
/*
* NB: Ideally this method should be annotated with @PortalTransactional,
* but (unfortunately) it doesn't work. There are multiple approaches to
* persistence (JPA and pre-JPA) at play in the concrete
* ITenantOperationsListener objects.
*/
// Invoke the listeners
final ITenant tenant = tenantDao.getTenantByFName(fname);
for (ITenantOperationsListener listener : this.tenantOperationsListeners) {
TenantOperationResponse res = null; // default
try {
res = listener.onDelete(tenant);
if (!TenantOperationResponse.Result.IGNORE.equals(res.getResult())) {
responses.add(res);
}
} catch (Exception e) {
final String msg =
"Error invoking ITenantOperationsListener '"
+ listener.toString()
+ "' for tenant: "
+ tenant.toString();
throw new RuntimeException(msg, e);
}
if (res != null && res.getResult().equals(TenantOperationResponse.Result.ABORT)) {
final String msg =
"ITenantOperationsListener '"
+ listener.toString()
+ "' aborted the operation for tenant: "
+ tenant.toString();
throw new RuntimeException(msg);
}
}
// Fire an appropriate PortalEvent
final HttpServletRequest request = portalRequestUtils.getCurrentPortalRequest();
tenantEventFactory.publishTenantCreatedTenantEvent(request, this, tenant);
}
/**
* List of the fnames of currently configured {@link ITenantOperationsListener} objects that may
* be omitted, presented in their natural (sequential) order.
*
* @since 4.3
*/
public List<ITenantOperationsListener> getOptionalOperationsListeners() {
return optionalOperationsListeners;
}
/**
* Complete set of actions from all listeners
*
* @since 4.3
*/
public Set<ITenantManagementAction> getAllAvaialableActions() {
return new HashSet<ITenantManagementAction>(operationsListenerAvailableActions.values());
}
/** @since 4.3 */
public ITenantManagementAction getAction(final String fname) {
// Assertions
if (StringUtils.isBlank(fname)) {
String msg = "Argument 'fname' cannot be blank";
throw new IllegalArgumentException(msg);
}
ITenantManagementAction rslt = this.operationsListenerAvailableActions.get(fname);
if (rslt == null) {
String msg = "Action not found: " + fname;
throw new RuntimeException(msg);
}
return rslt;
}
/**
* Returns true if a tenant with the specified name exists, otherwise false.
*
* @since 4.3
*/
public boolean nameExists(final String name) {
boolean rslt = false; // default
try {
final ITenant tenant = this.tenantDao.getTenantByName(name);
rslt = tenant != null;
} catch (IllegalArgumentException iae) {
// This exception is completely fine; it simply
// means there is no tenant with this name.
rslt = false;
}
return rslt;
}
/**
* Returns true if a tenant with the specified fname exists, otherwise false.
*
* @since 4.3
*/
public boolean fnameExists(final String fname) {
boolean rslt = false; // default
try {
final ITenant tenant = getTenantByFName(fname);
rslt = tenant != null;
} catch (IllegalArgumentException iae) {
// This exception is completely fine; it simply
// means there is no tenant with this fname.
rslt = false;
}
return rslt;
}
/**
* Throws an exception if the specified String isn't a valid tenant name.
*
* @since 4.3
*/
public void validateName(final String name) {
Validate.validState(
TENANT_NAME_VALIDATOR_PATTERN.matcher(name).matches(),
"Invalid tenant name '%s' -- names must match %s .",
name,
TENANT_NAME_VALIDATOR_REGEX);
}
/**
* Throws an exception if the specified String isn't a valid tenant fname.
*
* @since 4.3
*/
public void validateFname(final String fname) {
Validate.validState(
TENANT_FNAME_VALIDATOR_PATTERN.matcher(fname).matches(),
"Invalid tenant fname '%s' -- fnames must match %s .",
fname,
TENANT_FNAME_VALIDATOR_REGEX);
}
/**
* Throws an exception if any {@linkITenantOperationsListener} indicates that the specified
* value isn't allowable for the specified attribute.
*
* @throws Exception
* @since 4.3
*/
public void validateAttribute(final String key, final String value) throws Exception {
for (ITenantOperationsListener listener : tenantOperationsListeners) {
// Will throw an exception if not valid
listener.validateAttribute(key, value);
}
}
}