MultiTenant.java
/***************************************************************************
Copyright 2014 Emily Estes
Licensed 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
http://www.apache.org/licenses/LICENSE-2.0
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 net.metanotion.multitenant;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.metanotion.functor.Block;
import net.metanotion.sql.DbUtil;
import net.metanotion.sql.SchemaPoolSwitcher;
import net.metanotion.sql.SequenceExecutor;
import net.metanotion.util.JDBCTransaction;
import net.metanotion.util.MutableDictionary;
import net.metanotion.util.Unknown;
import net.metanotion.web.Instance;
import net.metanotion.web.AppFactory;
import net.metanotion.web.RequestObject;
import net.metanotion.web.SessionInitializer;
import net.metanotion.web.SessionFactory;
import net.metanotion.web.SessionStore;
import net.metanotion.web.concrete.FunctorSessionInitializer;
import net.metanotion.web.concrete.PrefixedInitializerMap;
import net.metanotion.web.concrete.URIPrefixDispatcher;
/** This class provides an implmentation of a "modular" multi-tenant web application and an associated control panel
to manage the tenants. A web application must implement the {@link net.metanotion.web.InstanceFactory} interface so
the control panel can create tenants on demand and at application start-up. A tenant implements the
{@link net.metanotion.web.Instance} interface. A tenant is assumed to be a "Straightforward" database driven
application. The tenant provides a set of SQL statements via the instance factory, and the multi-tenant component
provides tenant isolation by creating a PostgreSQL
<a href="http://www.postgresql.org/docs/9.3/static/ddl-schemas.html">schema</a> per tenant, and then providing
tenants with a {@link javax.sql.DataSource} that modify the PostgreSQL schema search_path. In addition, each tenant
lives at a URI "prefix", and a private session dictionary(see the generic parameter M and the
{@link net.metanotion.web.concrete.PrefixedInitializerMap} implementation and the
{@link net.metanotion.web.concrete.FunctorSessionInitializer} class to understand how session-isolation is
maintained.
@param <M> This is the type of the functor that translates request objects into Instance implementations. This type
must also implement MutableDictionary so that the multi-tenant component can add and remove instances to the
translation.
*/
public final class MultiTenant<M extends Block<RequestObject,Instance> & MutableDictionary<String,Instance>>
implements MultiTenantAdmin {
private static final Logger logger = LoggerFactory.getLogger(MultiTenant.class);
private final URIPrefixDispatcher dispatcher = new URIPrefixDispatcher();
private final DataSource ds;
private final M tenants;
/** Create a multi-tenant application with the default instance provider.
@param ds The master database connection pool for the application.
*/
public MultiTenant(final DataSource ds) {
this(ds, (M) new PrefixedInitializerMap<Instance>());
}
/** Create a multi-tenant application. This constructor lets you provide your own request transformer. The request
transformer is the key to how session isolation is maintained and which instance to use for a given request(in
other words, this enables a lot of flexibility, but also a lot of power, so be careful when providing your own).
@param ds The master database connection pool for the application.
@param requestTransformer The instance provider.
{@link net.metanotion.web.concrete.PrefixedInitializerMap}<Instance> is the default value.
*/
public MultiTenant(final DataSource ds, final M requestTransformer) {
this.ds = ds;
this.tenants = requestTransformer;
}
/** Retrieve the dispatcher instance for the entire multi-tenant application.
@return The dispatcher for the multi-tenant application.
*/
public URIPrefixDispatcher dispatcher() { return this.dispatcher; }
/** Retrieve the SessionInitializer instance for the entire multi-tenant application.
@param appFactory The tenant factory for the application.
@return The SessionInitializer to use for the app server.
*/
public SessionInitializer sessionInitializer(final MultiTenantAppFactory appFactory) {
this.startAll(appFactory);
final SessionFactory<Instance> sf = new SF(this, appFactory);
return new FunctorSessionInitializer<Instance>(sf, tenants);
}
/** Retrieve the SessionInitializer instance for the entire multi-tenant application.
@param appFactory The tenant factory for the application.
@param store The session store to store sessions in.
@return The SessionInitializer to use for the app server.
*/
public SessionInitializer sessionInitializer(final MultiTenantAppFactory appFactory, final SessionStore store) {
this.startAll(appFactory);
final SessionFactory<Instance> sf = new SF(this, appFactory);
return new FunctorSessionInitializer<Instance>(sf, tenants, store);
}
/** Retreieve the request transformater used by this multi-tenant application to look up tenants given a request object.
@return The transformer to convert requests in to instances.
*/
public Block<RequestObject,Instance> requestTransformer() { return tenants; }
private static final class SF implements SessionFactory<Instance> {
private final MultiTenant mt;
private final MultiTenantAppFactory factory;
public SF(final MultiTenant mt, final MultiTenantAppFactory factory) {
this.mt = mt;
this.factory = factory;
}
// SessionFactory
@Override public Unknown newSession(final Instance i) { return i.newSession(); }
}
private void startAll(final MultiTenantAppFactory factory) {
for(final String prefix: factory) {
try {
this.startInstance(prefix, factory);
} catch (final RuntimeException ex) {
logger.error("Failed start an instance. {}", ex);
}
}
}
private Instance startInstance(final DataSource instDS, final String prefix, final MultiTenantAppFactory factory) {
return startInstance(instDS, prefix, factory.appFactory(prefix), factory);
}
private Instance startInstance(final DataSource instDS,
final String prefix,
final AppFactory app,
final MultiTenantAppFactory factory) {
try {
final Instance inst = app.newInstance(instDS, prefix, factory);
tenants.put(prefix, inst);
dispatcher.addDispatcher(prefix, app.dispatcher());
return inst;
} catch (final RuntimeException ex) {
factory.startFailed(prefix, ex);
throw ex;
}
}
// MultiTenantAdmin
@Override public Instance startInstance(final String prefix, final MultiTenantAppFactory factory) {
return this.startInstance(new SchemaPoolSwitcher(ds, factory.schemaName(prefix)), prefix, factory);
}
@Override public Instance getInstance(final String prefix) { return tenants.get(prefix); }
@Override public Instance stopInstance(final String prefix) {
final Instance inst = tenants.remove(prefix);
dispatcher.removeDispatcher(prefix);
return inst;
}
@Override public void removeInstance(final String prefix, final MultiTenantAppFactory factory) {
try {
stopInstance(prefix);
try (final Connection conn = ds.getConnection()) {
DbUtil.dropSchema(conn, factory.schemaName(prefix), true);
}
} catch (final RuntimeException ex) {
factory.removeFailed(prefix, ex);
throw ex;
} catch (final SQLException sqle) {
factory.removeFailed(prefix, sqle);
throw new RuntimeException(sqle);
}
}
@Override public Instance setInstance(final String prefix, final AppFactory appFactory, final MultiTenantAppFactory factory) {
stopInstance(prefix);
return startInstance(new SchemaPoolSwitcher(ds, factory.schemaName(prefix)), prefix, appFactory, factory);
}
@Override public Instance createInstance(final String prefix, final MultiTenantAppFactory factory) {
try {
final DataSource instDS = new SchemaPoolSwitcher(ds, factory.schemaName(prefix));
final AppFactory app = factory.appFactory(prefix);
try (final Connection conn = ds.getConnection()) {
DbUtil.createSchema(conn, factory.schemaName(prefix), false);
}
JDBCTransaction.doTX(instDS, new Block<Connection,Integer>() {
@Override public Integer eval(final Connection conn) throws Exception {
return SequenceExecutor.doSequence(conn, app.openSchema());
}
});
return startInstance(instDS, prefix, factory);
} catch (final RuntimeException ex) {
factory.createFailed(prefix, ex);
throw ex;
} catch (final SQLException sqle) {
factory.createFailed(prefix, sqle);
throw new RuntimeException(sqle);
}
}
}