/** * Copyright (C) 2011 JTalks.org Team * This library 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 2.1 of the License, or (at your option) any later version. * This library 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 this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package org.jtalks.jcommune.service.security.acl; import org.apache.commons.lang.Validate; import org.jtalks.common.model.entity.*; import org.springframework.security.acls.domain.ObjectIdentityImpl; import org.springframework.security.acls.model.ObjectIdentity; import javax.annotation.Nonnull; import javax.annotation.concurrent.ThreadSafe; import javax.validation.constraints.Min; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * If we don't want to have Object Identity (OID) in the database having the same type as the entity class, we can have * this generator being initialized with conversion rules so that some other value goes to database. <br/><br/> * <b>Example</b>: You're saving an instance of {@link Branch} as an Object Identity to the database into ACL tables. If * you use usual {@link org.springframework.security.acls.model.ObjectIdentityRetrievalStrategy}, then you'll have in * your {@code acl_class} table a record with {@link Branch} value. But if you want to * have a custom type being saved there, let's say {@code BRANCH_CLASS}, you can use this generator and set a conversion * rule <u>{@code Branch.class -> BRANCH_CLASS}</u> and the latter will be saved into database. <br/><br/><b>When we * need this</b>: Since we have a number of projects working with the same database (like Poulpe and JCommune), they all * have their own custom classes for instance for {@link Branch} and thus if we use a default OID generator, we'll end * up having Poulpe saving its own Branch with its own class to the database, and JCommune can't read it since it has * its own Branch and it expects a different value stored in {@code acl_class} table. But if we use different classes * and we covert their class name to the same {@code acl_class}, then we can use the same ACL records in Poulpe and * JCommune. This is required because Poulpe gives the permissions to Branches and JCommune needs to read those * permissions. <br/><br/><b>Other notes:</b> <ul><li>This class will be empty without any rules if created with default * constructor, thus if you want to ensure you use the same string constants for all the projects, you should use {@link * #createDefaultGenerator()} which contains mapping for entities in common modules and fill it with additional rules * specific to your project.</li> <li>While specifying the conversion rules for the class, not only the very specified * class is going to be converted to the specific string, but also if the object is of assignable class ({@code object * instanceof Class or any of its ancestors} ), then this conversion rule still is going to be applied. Thus if the rule * states Branch->BRANCH, then PoulpeBranch is also going to be converted to BRANCH. This is also important because of * Hibernate that creates proxies </li></ul> * * @author stanislav bashkirtsev */ @ThreadSafe public class TypeConvertingObjectIdentityGenerator { /** * Creates an instance of {@link ObjectIdentity} from the specified id of the entity and the type of it (it's not * necessary a class name). * * @param id a database id of the entity we want to create an object identity for, must be greater than 0 * @param type a type of the entity, it can be either a class or a result of conversion rule (e.g. either * {@link Branch} or <u>BRANCH_CLASS</u> or anything else, see class * JavaDocs for more details. Note, that if a conversion rules are applied for this entity and you're * specifying the wrong type here, you'll end-up with an invalid Object Identity that can't be found in * database because its type is absent in {@code acl_class}. * @return an object identity constructed from the specified id of the entity and the entity type (either a simple * class name or a converted value) * @throws IllegalArgumentException if the specified id is zero or less (aka not persisted yet) * @see #addConversionRule(Class, String) * @see #setAdditionalConversionRules */ public ObjectIdentity createObjectIdentity(@Min(1) long id, String type) { Validate.isTrue(id > 0, "Entity must be persisted before creating Secured OID for it!"); return new ObjectIdentityImpl(type, id); } /** * Creates an Object Identity (OID) for the specified entity using the conversion rules set into generator. * * @param domainObject an entity that is stored into a database and which is going to be an Object Identity * (permissions will be assigned to SID to do something with this object). Must be persisted * already and have its id. * @return an object identity with the ID of specified entity and a type according to the conversion rules. If no * conversion rule was found for the class of specified object, then it's {@link * Object#getClass()#getSimpleName} will be used as the OID type). * @throws IllegalArgumentException if the specified domain object is not persistent (it's ID is not a positive * number) * @see #addConversionRule(Class, String) * @see #setAdditionalConversionRules */ public ObjectIdentity getObjectIdentity(@Nonnull Entity domainObject) { String type = getType(domainObject); return createObjectIdentity(domainObject.getId(), type); } /** * You don't usually need an empty OID generator since there are common entities with common names that are shared * between projects within JTalks, that's why you can use this factory method that creates a generator with already * filled values, see the method body to understand for what classes the conversion rules are set. Afterwards you * can fill the class with your custom classes specific only to your project. Note, that if an entity was placed in * the common modules of JTalks, it might make sense to add it to this method. * * @return an OID generator with already filled type conversion rules */ public static TypeConvertingObjectIdentityGenerator createDefaultGenerator() { return new TypeConvertingObjectIdentityGenerator() .addConversionRule(Group.class, "GROUP") .addConversionRule(Branch.class, "BRANCH") .addConversionRule(Section.class, "SECTION") .addConversionRule(Component.class, "COMPONENT"); } /** * Adds additional conversion rule to the generator. * * @param entityClass a class to covert its name to the string * @param convertTo a string the specified class will be converted while creating an Object Identity * @return this * @see #setAdditionalConversionRules */ public TypeConvertingObjectIdentityGenerator addConversionRule(Class entityClass, String convertTo) { oidClassToTypeMap.put(entityClass, convertTo); return this; } /** * Iterates through all the conversion rules and searches for the one applicable to the specified entity. If a * conversion rule was set via {@link #addConversionRule(Class, String)} or {@link #setAdditionalConversionRules}, * then the string will be returned representing the type of specified object, otherwise a {@link * Class#getSimpleName()} of the specified object will be returned. * * @param domainObject an object to find its conversion rule or to generate it from its class's simple name * @return a type of the object identity according to the conversion rules or the class's simple name if no rule was * found for this entity */ private String getType(Object domainObject) { for (Map.Entry<Class, String> nextConversionPair : oidClassToTypeMap.entrySet()) { if (nextConversionPair.getKey().isInstance(domainObject)) { return nextConversionPair.getValue(); } } return domainObject.getClass().getSimpleName(); } /** * Adds the specified conversion rules to the generator. <b>Note</b>, that it doesn't remove old rules, but it can * override them if the key value of the map will be the same. * * @param oidClassToTypeMap see {@link #addConversionRule(Class, String)} for what each entry of the map means */ public void setAdditionalConversionRules(@Nonnull Map<Class, String> oidClassToTypeMap) { this.oidClassToTypeMap.putAll(oidClassToTypeMap); } private final Map<Class, String> oidClassToTypeMap = new ConcurrentHashMap<Class, String>(); }