All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sirius.biz.web.BizController Maven / Gradle / Ivy

There is a newer version: 9.6
Show newest version
/*
 * Made with all the love in the world
 * by scireum in Remshalden, Germany
 *
 * Copyright by scireum GmbH
 * http://www.scireum.de - [email protected]
 */

package sirius.biz.web;

import sirius.biz.tenants.Tenant;
import sirius.biz.tenants.TenantAware;
import sirius.biz.tenants.Tenants;
import sirius.biz.tenants.UserAccount;
import sirius.db.mixing.Column;
import sirius.db.mixing.Entity;
import sirius.db.mixing.EntityRef;
import sirius.db.mixing.OMA;
import sirius.db.mixing.Property;
import sirius.db.mixing.properties.BooleanProperty;
import sirius.db.mixing.properties.EntityRefProperty;
import sirius.kernel.commons.Strings;
import sirius.kernel.di.std.ConfigValue;
import sirius.kernel.di.std.Part;
import sirius.kernel.health.Exceptions;
import sirius.kernel.health.HandledException;
import sirius.kernel.health.Log;
import sirius.kernel.nls.Formatter;
import sirius.web.controller.BasicController;
import sirius.web.controller.Message;
import sirius.web.http.WebContext;
import sirius.web.security.UserContext;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * Base class for all controllers which operate on entities.
 * 

* Provides glue logic for filling entites from {@link WebContext}s and for resolving entities for a given id. * * @see Entity */ public class BizController extends BasicController { @Part protected OMA oma; @Part protected Tenants tenants; @ConfigValue("product.baseUrl") private String baseUrl; private static boolean baseUrlChecked; /** * Contains the central logger for biz-relatet messages. */ public static final Log LOG = Log.get("biz"); /** * Ensures that the tenant of the current user matches the tenant of the given entity. * * @param tenantAware the entity to check * @throws sirius.kernel.health.HandledException if the tenants do no match */ protected void assertTenant(TenantAware tenantAware) { if (tenantAware == null) { return; } if (currentTenant() == null && tenantAware.getTenant().getId() != null) { throw invalidTenantException(); } if (currentTenant().getId() != tenantAware.getTenant().getId()) { throw invalidTenantException(); } } private HandledException invalidTenantException() { return Exceptions.createHandled().withNLSKey("BizController.invalidTenant").handle(); } /** * Checks if the tenant aware entity belongs to the current tenant or to its parent tenant. * * @param tenantAware {@link TenantAware} entity to be asserted */ protected void assertTenantOrParentTenant(TenantAware tenantAware) { if (tenantAware == null) { return; } if (!tenantAware.getTenant().is(currentTenant()) && !Objects.equals(tenantAware.getTenant().getId(), currentTenant().getParent().getId())) { throw invalidTenantException(); } } /** * Enusures or establishes a parent child relation. *

* For new entities (owner), the given reference is initialized with the given entity. For existing entities * it is verified, that the given reference points to the given entity. * * @param owner the entity which contains the reference * @param ref the reference which is either filled or verified that it points to entity * @param entity the entity the reference must point to * @param the generic type the the entity being referenced * @throws sirius.kernel.health.HandledException if the entities do no match */ protected void setOrVerify(Entity owner, EntityRef ref, E entity) { if (!Objects.equals(ref.getId(), entity.getId())) { if (owner.isNew()) { ref.setValue(entity); } else { throw Exceptions.createHandled().withNLSKey("BizController.invalidReference").handle(); } } } /** * Ensures that the given entity is already persisted in the database. * * @param obj the entity to check * @throws sirius.kernel.health.HandledException if the entity is still new and not yet persisted in the database */ protected void assertNotNew(Entity obj) { assertNotNull(obj); if (obj.isNew()) { throw Exceptions.createHandled().withNLSKey("BizController.mustNotBeNew").handle(); } } /** * Returns the base URL of this instance. * * @return the base URL like http://www.mydomain.stuff */ protected String getBaseUrl() { if (!baseUrlChecked) { baseUrlChecked = true; if (Strings.isEmpty(baseUrl)) { LOG.WARN("product.baseUrl is not filled. Please update the system configuration!"); } } return baseUrl; } /** * Fetches all autoloaded fields of the given entity from the given request and populates the entity. * * @param ctx the request to read parameters from * @param entity the entity to fill * @see Autoloaded */ protected void load(WebContext ctx, Entity entity) { List columns = entity.getDescriptor() .getProperties() .stream() .filter(property -> shouldAutoload(ctx, property)) .map((Property property) -> { return Column.named(property.getName()); }) .collect(Collectors.toList()); load(ctx, entity, columns); } /** * Reads the given properties from the given request and populates the given entity. * * @param ctx the request to read parameters from * @param entity the entity to fill * @param properties the list of properties to transfer */ protected void load(WebContext ctx, Entity entity, Column... properties) { load(ctx, entity, Arrays.asList(properties)); } protected void load(WebContext ctx, Entity entity, List properties) { boolean hasError = false; for (Column columnProperty : properties) { Property property = entity.getDescriptor().getProperty(columnProperty); String propertyName = property.getName(); try { property.parseValue(entity, ctx.get(propertyName)); ensureTenantMatch(entity, property); } catch (HandledException e) { UserContext.setFieldError(propertyName, ctx.get(propertyName)); UserContext.setErrorMessage(propertyName, e.getMessage()); hasError = true; } } if (hasError) { throw Exceptions.createHandled().withNLSKey("BizController.illegalArgument").handle(); } } private void ensureTenantMatch(Entity entity, Property property) { if ((entity instanceof TenantAware) && property instanceof EntityRefProperty) { Object loadedEntity = property.getValue(entity); if (loadedEntity instanceof TenantAware) { ((TenantAware) entity).assertSameTenant(property::getLabel, (TenantAware) loadedEntity); } } } private boolean shouldAutoload(WebContext ctx, Property property) { if (!isAutoloaded(property)) { return false; } // If the parameter is present in the request we're good to go if (ctx.hasParameter(property.getName())) { return true; } // If the property is a boolean one, it will most probably handled // by a checkbox. As an unchecked checkbox will not submit any value // we still process this property, which is then considered to be // false (matching the unchecked checkbox). return property instanceof BooleanProperty; } private boolean isAutoloaded(Property property) { Autoloaded autoloaded = property.getAnnotation(Autoloaded.class); if (autoloaded == null) { return false; } if (autoloaded.permissions().length > 0) { return UserContext.getCurrentUser().hasPermissions(autoloaded.permissions()); } else { return true; } } /** * Provides a fluent API to control the process and user routing while creating or updating an entity in the * database. */ public class SaveHelper { private WebContext ctx; private Consumer preSaveHandler; private Consumer postSaveHandler; private String createdURI; private String afterSaveURI; private List columns; private boolean autoload = true; private SaveHelper(WebContext ctx) { this.ctx = ctx; } /** * Installs a pre save handler which is invoked just before the entity is persisted into the database. * * @param preSaveHandler a consumer which is supplied with a boolean flag, indicating if the entity was new. * The handler can be used to modify the entity before it is saved. * @return the helper itself for fluent method calls */ public SaveHelper withPreSaveHandler(Consumer preSaveHandler) { this.preSaveHandler = preSaveHandler; return this; } /** * Installs a post save handler which is invoked just after the entity was persisted into the database. * * @param postSaveHandler a consumer which is supplied with a boolean flag, indicating if the entiy was new. * The * handler can be used to modify the entity or related entities after it was created in * the database. * @return the helper itself for fluent method calls */ public SaveHelper withPostSaveHandler(Consumer postSaveHandler) { this.postSaveHandler = postSaveHandler; return this; } /** * Specifies what columns should be loaded from the request context *

* if not set all marked as {@link Autoloaded} properties of the entity are loaded * * @param columns array of {@link Column} objects * @return the helper itself for fluent method calls */ public SaveHelper withColumns(Column... columns) { this.columns = Arrays.asList(columns); return this; } /** * Used to supply a URL to which the user is redirected if a new entity was created. *

* As new entities are often created using a placeholder URL like /entity/new, we must * redirect to the canonical URL like /entity/128 if a new entity was created. *

* Note that the redirect is only performed if the newly created entity has validation warnings or the Entity is * new. * * @param createdURI the URI to redirect to where ${id} is replaced with the actual id of the entity * @return the helper itself for fluent method calls */ public SaveHelper withAfterCreateURI(String createdURI) { this.createdURI = createdURI; return this; } /** * Used to supply a URL to which the user is redirected if an entity was successfully saved. *

* Once an entity was successfully saved is not new and has no validation warnings, the user will be redirected * to the given URL. * * @param afterSaveURI the list or base URL to return to, after an entity was successfully edited. * @return the helper itself for fluent method calls */ public SaveHelper withAfterSaveURI(String afterSaveURI) { this.afterSaveURI = afterSaveURI; return this; } /** * Disables the automatically loading process of all entity properties annotated with {@link Autoloaded}. * * @return the helper itself for fluent method calls */ public SaveHelper disableAutoload() { this.autoload = false; return this; } /** * Applies the configured save login on the given entity. * * @param entity the entity to update and save * @return true if the request was handled (the user was redirected), false otherwise */ public boolean saveEntity(Entity entity) { if (!ctx.isPOST()) { return false; } try { boolean wasNew = entity.isNew(); if (autoload) { load(ctx, entity); } if (columns != null && !columns.isEmpty()) { load(ctx, entity, columns); } if (preSaveHandler != null) { preSaveHandler.accept(wasNew); } oma.update(entity); if (postSaveHandler != null) { postSaveHandler.accept(wasNew); } if (wasNew && Strings.isFilled(createdURI)) { ctx.respondWith() .redirectToGet(Formatter.create(createdURI).set("id", entity.getIdAsString()).format()); return true; } if (!oma.hasValidationWarnings(entity) && Strings.isFilled(afterSaveURI)) { ctx.respondWith() .redirectToGet(Formatter.create(afterSaveURI).set("id", entity.getIdAsString()).format()); return true; } showSavedMessage(); } catch (Exception e) { UserContext.handle(e); } return false; } } /** * Creates a {@link SaveHelper} with provides a fluent API to save an entity into the database. * * @param ctx the current request * @return a helper used to configure the save process */ protected SaveHelper prepareSave(WebContext ctx) { return new SaveHelper(ctx); } /** * Performs a validation and reports all warnings via the {@link UserContext}. * * @param entity the entity to validate */ protected void validate(Entity entity) { for (String warning : oma.validate(entity)) { UserContext.message(Message.warn(warning)); } } /** * Tries to find an entity of the given type with the given id. *

* Note, if new is given as id, a new entity is created. This permits many editors to create a * new entity simply by calling /editor-uri/new * * @param type the type of the entity to find * @param id the id to lookup * @param the generic type of the entity class * @return the requested entity or a new one, if id was new * @throws sirius.kernel.health.HandledException if either the id is unknown or a new instance cannot be created */ protected E find(Class type, String id) { if (Entity.NEW.equals(id) && Entity.class.isAssignableFrom(type)) { try { return type.newInstance(); } catch (Exception e) { throw Exceptions.handle() .to(LOG) .error(e) .withSystemErrorMessage("Cannot create a new instance of '%s'", type.getName()) .handle(); } } Optional result = oma.find(type, id); if (!result.isPresent()) { throw Exceptions.createHandled().withNLSKey("BizController.unknownObject").set("id", id).handle(); } return result.get(); } /** * Tries to find an existing entity with the given id. * * @param type the type of the entity to find * @param id the id of the entity to find * @param the generic type of the entity class * @return the requested entity wrapped as Optional or an empty optional, if no entity with the given id * was found * or if the id was new */ protected Optional tryFind(Class type, String id) { if (Entity.NEW.equals(id)) { return Optional.empty(); } return oma.find(type, id); } /** * Tries to find an entity for the given id, which belongs to the current tenant. *

* This behaves just like {@link #find(Class, String)} but once an existing entity was found, which also extends * {@link TenantAware}, it is ensured (using {@link #assertTenant(TenantAware)} that it belongs to the current * tenant. * * @param type the type of the entity to find * @param id the id of the entity to find * @param the generic type of the entity class * @return the requested entity, which is either new or belongs to the current tenant */ protected E findForTenant(Class type, String id) { E result = find(type, id); if (result instanceof TenantAware) { if (result.isNew()) { ((TenantAware) result).getTenant().setValue(currentTenant()); } else { assertTenant((TenantAware) result); } } return result; } /** * Tries to find an entity for the given id, which belongs to the current tenant. *

* This behaves just like {@link #tryFind(Class, String)} but once an existing entity was found, which also extends * {@link TenantAware}, it is ensured (using {@link #assertTenant(TenantAware)} that it belongs to the current * tenant. * * @param type the type of the entity to find * @param id the id of the entity to find * @param the generic type of the entity class * @return the requested entity, which belongs to the current tenant, wrapped as Optional or an empty * optional. */ protected Optional tryFindForTenant(Class type, String id) { return tryFind(type, id).map(e -> { if (e instanceof TenantAware) { assertTenant((TenantAware) e); } return e; }); } /** * Returns the {@link UserAccount} instance which belongs to the current user. * * @return the UserAccount instance of the current user or null if no user is logged in */ protected UserAccount currentUser() { if (!UserContext.getCurrentUser().isLoggedIn()) { return null; } return UserContext.getCurrentUser().getUserObject(UserAccount.class); } /** * Returns the {@link Tenant} instance which belongs to the current user. * * @return the Tenant instance of the current user or null if no user is logged in */ protected Tenant currentTenant() { if (!UserContext.getCurrentUser().isLoggedIn()) { return null; } return currentUser().getTenant().getValue(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy