| [//]: # " " |
| [//]: # " Copyright (c) 2013, 2021 Oracle and/or its affiliates. All rights reserved. " |
| [//]: # " " |
| [//]: # " This program and the accompanying materials are made available under the " |
| [//]: # " terms of the Eclipse Public License v. 2.0, which is available at " |
| [//]: # " http://www.eclipse.org/legal/epl-2.0. " |
| [//]: # " " |
| [//]: # " This Source Code may also be made available under the following Secondary " |
| [//]: # " Licenses when the conditions for such availability set forth in the " |
| [//]: # " Eclipse Public License v. 2.0 are satisfied: GNU General Public License, " |
| [//]: # " version 2 with the GNU Classpath Exception, which is available at " |
| [//]: # " https://www.gnu.org/software/classpath/license.html. " |
| [//]: # " " |
| [//]: # " SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 " |
| [//]: # " " |
| |
| # Tenant Managed Scope Example |
| |
| This example illustrates how a scope can be made that is tenant aware. First, lets define a tenant. |
| |
| A tenant is a an entity who has certain attributes that are backed by an XML configuration file. |
| The XML configuration file will contain all the information about that tenant. |
| For example, this XML contains all the data about a Tenant named Alice |
| |
| ```xml |
| <environments> |
| <environment name="Alice" minSize="1" maxSize="2" /> |
| </environments> |
| ``` |
| |
| This XML contains all the data about a tenant named Bob |
| |
| ```xml |
| <environments> |
| <environment name="Bob" minSize="10" maxSize="20" /> |
| </environments> |
| ``` |
| |
| The CTM team would like to have an interface called Environment that encapsulates all the information about a tenant: |
| |
| ```java |
| public interface Environment { |
| String getName(); |
| int getMinSize(); |
| int getMaxSize(); |
| } |
| ``` |
| |
| There is code in the system that would like to inject the Environment class. However, the code that will inject |
| this interface is in the Singleton scope, which means it never changes. And yet, depending on the state of the system |
| the underlying tenant may change from Alice to Bob, or to any other tenant. For example, the class could look like |
| this: |
| |
| ```java |
| @Singleton |
| public class ServiceProvisioningEngine { |
| |
| @Inject |
| private Environment tenant; |
| |
| private void provisionServices() { |
| //envs here could be either env1 or env2 depending on |
| // whether this code is being executed on behalf of either tenant1 OR tenant2 |
| } |
| |
| } |
| ``` |
| |
| We would like to demonstrate how a custom scope, called the TenantScope, can be used to solve this problem. Firstly, in order |
| to solve the problem of the Singleton service getting injected with the Environment only at construction time, we must have HK2 |
| inject a proxy for the Environment service rather than the particular environment itself. The proxy can figure out, depending |
| on the state of the system, which tenant is active, and then use the proper Environment object for the real call. In order to |
| ensure that this proxy is created, the TenantScope must be Proxiable. You mark a scope as being Proxiable by adding the @Proxiable |
| annotation to the definition of the scope annotation itself, like this: |
| |
| ```java |
| @Scope |
| @Proxiable |
| @Retention(RUNTIME) |
| @Target( { TYPE, METHOD }) |
| public @interface TenantScoped { |
| } |
| ``` |
| |
| This annotation is now marked as a scope indicator (via the @Scope annotation) and as proxiable (via the @Proxiable annotation). All |
| objects that are injected from this scope will be given a proxy that uses the underlying machinery to determine the state of the system |
| and to use the proper backing objects for the real calls. |
| |
| Now, however, we need some way to tell the system what tenant is currently active. For this example, we have created a |
| class called the TenantManager. The TenantManager is in the Singleton scope which means it will only be created once (when the first |
| person demands one). The job of the TenantManager is to be able to set the currently active tenant. It does so with this call: |
| |
| ```java |
| @Service |
| public class TenantManager { |
| private String currentTenant; |
| |
| public void setCurrentTenant(String currentTenant) { |
| this.currentTenant = currentTenant; |
| } |
| |
| public String getCurrentTenant() { |
| return currentTenant; |
| } |
| } |
| ``` |
| |
| OK, great, now we know how to change the current tenant that is running on the system. But how do we make the system understand this? |
| Well, since we decided that we were going to have a proxiable scope, we also need a corresponding implementation of the Context interface. |
| You do this by implementing Context, and making sure that the parameterized type of the Context is the class of the scope annotation. |
| In this case, we would implement the context for the TenantScope scope like this: |
| |
| ```java |
| @Singleton |
| public class TenantScopedContext implements Context<TenantScoped> { |
| @Inject |
| private TenantManager manager; |
| ... |
| ``` |
| |
| Note that the implementation of Context is itself a service, in the Singleton scope (there is nothing that says a Context must be |
| in the Singleton scope, but most probably would be). The only rule is that a Context implementation cannot be in the same scope as |
| the scope it is backing. Also notice that since Context is a regular service that it can be injected with other services, such |
| as the TenantManager. Luckily, that is just what we need, since the TenantManager knows what the current scope is! First, lets |
| see what the implementation of the isActive method of the TenantScopedContext would look like: |
| |
| ```java |
| public boolean isActive() { |
| return manager.getCurrentTenant() != null; |
| } |
| ``` |
| |
| That was pretty easy. But now lets think about what the TenantScopeContext has to do. It must keep the set of objects created |
| per tenant. For example, we do not want to create the Environment implementation for the Alice tenant more than once. And |
| so the TenantScopeContext keeps a mapping for each tenant. Here is the map, and the code that gets the proper mapping based |
| on the current tenant: |
| |
| ```java |
| private final HashMap<String, HashMap<ActiveDescriptor<?>, Object>> contexts = new HashMap<String, HashMap<ActiveDescriptor<?>, Object>>(); |
| |
| private HashMap<ActiveDescriptor<?>, Object> getCurrentContext() { |
| if (manager.getCurrentTenant() == null) throw new IllegalStateException("There is no current tenant"); |
| |
| HashMap<ActiveDescriptor<?>, Object> retVal = contexts.get(manager.getCurrentTenant()); |
| if (retVal == null) { |
| retVal = new HashMap<ActiveDescriptor<?>, Object>(); |
| |
| contexts.put(manager.getCurrentTenant(), retVal); |
| } |
| |
| return retVal; |
| } |
| ``` |
| |
| Based on this code, it is now easy to write the find method of the TenantScopedContext: |
| |
| ```java |
| public <T> T find(ActiveDescriptor<T> descriptor) { |
| HashMap<ActiveDescriptor<?>, Object> mappings = getCurrentContext(); |
| |
| return (T) mappings.get(descriptor); |
| } |
| ``` |
| |
| The method that does findOrCreate is also fairly simple to write now. If it cannot find the service in the mapping |
| for this tenant, then it must create one using the create method of the ActiveDescriptor, passing in the root. The |
| passing in of the root allows for objects of scope PerLookup to be destroyed properly when this object gets destroyed. |
| |
| ```java |
| public <T> T findOrCreate(ActiveDescriptor<T> activeDescriptor, |
| ServiceHandle<?> root) { |
| HashMap<ActiveDescriptor<?>, Object> mappings = getCurrentContext(); |
| |
| Object retVal = mappings.get(activeDescriptor); |
| if (retVal == null) { |
| retVal = activeDescriptor.create(root); |
| |
| mappings.put(activeDescriptor, retVal); |
| } |
| |
| return (T) retVal; |
| } |
| ``` |
| |
| That is it for writing the Context implementation for the TenantScope! At this point, objects will be created in the |
| tenant scope only when no object has already been created for that tenant. And when a tenant is switched, a new mapping |
| is generated and we start the process all over again. |
| |
| That is all well and good, but the question of how these objects are truly created for the TenantScope is still unclear. For example, |
| we need for the Alice Environment implementation to be produced when the Alice tenant is active, and we need the Bob Environment |
| to be produced when the Bob tenant is active. |
| |
| In order to achieve this goal, we create a factory of Environments. A factory can be used to create objects when some criteria |
| that cannot be easily expressed as an injection point needs to be taken into account. Factories |
| can produce things into any scope and with any qualifiers. The Factory interface has a method on it called produce(), which |
| can be annotated with the scope and qualifiers that are to be associated with the produced objects. |
| |
| We then implement an EnvironmentFactory, giving the type we are producing as the actual type in the parameterized type, like this: |
| |
| ```java |
| @Singleton |
| public class EnvironmentFactory implements Factory<Environment> { |
| @Inject |
| private TenantManager manager; |
| |
| ... |
| } |
| ``` |
| |
| It is interesting to notice that the factory itself is a service in the Singleton scope, and hence can be injected with the |
| TenantManager. However, that does not mean that the EnvironmentFactory is producing objects into the Singleton scope, only that the |
| factory itself is in the Singleton scope. We tell the system that this factory is producing items for the TenantScope by annotating |
| the produce method, like this: |
| |
| ```java |
| @TenantScoped |
| public Environment provide() {...} |
| ``` |
| |
| In this example, we are going to use other ServiceLocator registry's in order to create the specific Environment objects that we need. |
| We will have a new ServiceLocator registry for each tenant, and that new ServiceLocator will be responsible for instantiating |
| and providing the Environment implementations that we require. |
| |
| ServiceLocators will be used as delegate for Habitat to create config instance of Environments based on a |
| particular backing XML file, using the configuration subsystem of hk2. |
| |
| Thus the job of the EnvironmentFactory is to keep a map from tenants to their backing ServiceLocator registries. Here is |
| the mapping and the code that associates a particular ServiceLocator with the current tenant: |
| |
| ```java |
| private final HashMap<String, ServiceLocator> backingLocators = new HashMap<String, ServiceLocator>(); |
| private final TenantLocatorGenerator generator = new TenantLocatorGenerator(); |
| |
| private ServiceLocator getCurrentLocator() { |
| if (manager.getCurrentTenant() == null) throw new IllegalStateException("There is no current tenant"); |
| |
| ServiceLocator locator = backingLocators.get(manager.getCurrentTenant()); |
| if (locator == null) { |
| locator = createNewLocator(); |
| backingLocators.put(manager.getCurrentTenant(), locator); |
| } |
| |
| return locator; |
| } |
| |
| private ServiceLocator createNewLocator() { |
| return generator.generateLocatorPerTenant(manager.getCurrentTenant()); |
| } |
| ``` |
| |
| The job of the TenantLocatorGenerator is to create a new ServiceLocator based on the current tenant and populate it |
| with values from a backing XML file. |
| |
| ```java |
| ServiceLocator serviceLocator = factory.create(tenantName, parent); |
| |
| // Will add itself to serviceLocator by tenantName |
| Habitat h = new Habitat(null, tenantName); |
| |
| // Populate this serviceLocator with config data. |
| for (Populator p : serviceLocator.<Populator>getAllServices(Populator.class)) { |
| p.run(new ConfigParser(h)); |
| } |
| ``` |
| |
| Then it is necessary to implement Populator service, as follows below. Note, Habitat is backed by ServiceLocator for tenant. |
| |
| ```java |
| @Service |
| public class EnvironmentXml implements Populator { |
| @Inject |
| TenantManager tenantManager; |
| |
| @Inject |
| protected Habitat habitat; |
| |
| @Override |
| public void run(ConfigParser parser) throws ConfigPopulatorException { |
| String tenantName = tenantManager.getCurrentTenant(); |
| URL source = URL<tenantName.xml> |
| parser.parse(source, new DomDocument(habitat)); |
| } |
| |
| } |
| ``` |
| |
| The code of the produce method in the EnvironmentFactory is now straightforward: |
| |
| ```java |
| @TenantScoped |
| public Environment provide() { |
| ServiceLocator locator = getCurrentLocator(); |
| |
| return locator.getService(Environment.class); |
| } |
| ``` |
| |
| Voila! We have a fairly simple example of how to create a TenantContext that meets the original requirements. We have |
| created a TenantScope/TenantContext pair, and made sure it only creates objects when no object already exists for a certain |
| tenant. We have also seen how to write a factory which knows how to produce Environment objects based on the currently |
| active tenant. |
| |
| Now, lets take a look at the test, and how the test validates the original requirement, which was that the |
| ServiceProviderEngine should be using the proper tenant based on the current state of the system, without having |
| to be re-injected. Here is the pseudo-code for the test: |
| |
| ```java |
| TenantManager tenantManager = locator.getService(TenantManager.class); |
| ServiceProviderEngine engine = locator.getService(ServiceProviderEngine.class); |
| |
| tenantManager.setCurrentTenant(TenantLocatorGenerator.ALICE); |
| |
| // Validate that the engine is using the ALICE tenant |
| |
| tenantManager.setCurrentTenant(TenantLocatorGenerator.BOB); |
| |
| // Validate that the engine is using the BOB tenant |
| ``` |
| |
| The point of the test is to ensure that the Environment object passed into the ServiceProviderEngine is in fact getting switched |
| when we switch the tenant from Alice to Bob. |
| |