AdminAppFactory.java

/***************************************************************************
   Copyright 2013 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.adminapp;


import java.io.IOException;
import java.sql.Connection;
import java.util.Arrays;
import java.util.Iterator;
import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.metanotion.authident.UserToken;
import net.metanotion.email.MailerProxy;
import net.metanotion.formsauth.AuthAPI;
import net.metanotion.formsauth.AuthFactory;
import net.metanotion.formsauth.AuthFsm;
import net.metanotion.formsauth.AuthMailer;
import net.metanotion.formsauth.AuthStates;
import net.metanotion.formsauth.FormsAuth;
import net.metanotion.formsauth.FormsAuthAPI;
import net.metanotion.functor.Block;
import net.metanotion.io.ClassLoaderFileSystem;
import net.metanotion.io.File;
import net.metanotion.io.FileSystem;
import net.metanotion.multitenant.MultiTenantAdmin;
import net.metanotion.multitenant.MultiTenantAppFactory;
import net.metanotion.scripting.ObjectServer;
import net.metanotion.scripting.Scriptable;
import net.metanotion.scripting.ScriptingObjectMagic;
import net.metanotion.simpletemplate.ChainedResourceFactory;
import net.metanotion.simpletemplate.MountPointResources;
import net.metanotion.simpletemplate.ResourceDispatcher;
import net.metanotion.simpletemplate.ResourceFactory;
import net.metanotion.simpletemplate.StaticResources;
import net.metanotion.simpletemplate.TemplateResources;
import net.metanotion.sql.SchemaGenerator;
import net.metanotion.sql.SequenceExecutor;
import net.metanotion.sqlauthident.SQLRealm;
import net.metanotion.util.ChainedIterator;
import net.metanotion.util.Dispatcher;
import net.metanotion.util.Extends;
import net.metanotion.util.JDBCTransaction;
import net.metanotion.util.Service;
import net.metanotion.util.StateMachine;
import net.metanotion.util.Unknown;
import net.metanotion.util.UnknownMagic;
import net.metanotion.web.AppFactory;
import net.metanotion.web.Instance;
import net.metanotion.web.RequestObject;
import net.metanotion.web.Session;
import net.metanotion.web.concrete.JsonUtil;
import net.metanotion.web.concrete.URIPrefixDispatcher;

/** <p>This class implements the AppFactory interface for a basic multitenant tenant-administration web application.
This web application provides basic tenant-instance control functionality in the form of a web app with a simple
security model for tenant ownership and administration, and this app and some of it's interfaces, classes, and JS/HTML
can be used as a basis for developing a tenant-instance control panel for a multi-tenant Software as a Service(SaaS)
application.</p>

<p>This is a multiuser web application. By default, each user can create as many instances of the default tenant
instance application as they desire. Every tenant instance is owned by one (and only one) user. Users can transfer
ownership to other user accounts. The per-tenant control panel allows other users to be added as administrator's to the
instance by specifying an email address(which may or may not be currently linked to a user account in this web
application, this is an intentional design decision to specify administrator's by email/username rather than account
id). In addition, if the tenant instances (via the AppFactory interface's Unknown.lookupInterface() method) expose the
management interfaces: {@link net.metanotion.multitenant.TenantExport} and
{@link net.metanotion.multitenant.TenantUsers} the instance control panel also provides a user interface for accessing
those api's.</p> */
public final class AdminAppFactory implements AppFactory {
	private static final Logger logger = LoggerFactory.getLogger(AdminAppFactory.class);
	private static final Queries tenantQueries = new Queries();

	/** This interface serves as a method for component clients to provide configuration details in one object on
	demand. */
	public interface ResourceFactoryConfiguration {
		/** This resource factory provides the templates and files to create the multi-tenant admin pages for
			authenticated users.
			@return a resource factory for authenticated admin users.
		*/
		public ResourceFactory userResources();

		/** This resource factory provides the templates and files to create the multi-tenant admin pages for
		non-authenticated users.
			@return a resource factory for non-authenticated admin users.
		*/
		public ResourceFactory anonResources();
	}

	/** This is a default implementation of the {@link ResourceFactoryConfiguration} interface using the JS/HTML assets
	embedded in the framework jar file. */
	public static final ResourceFactoryConfiguration DEFAULT_RESOURCES = new ResourceFactoryConfiguration() {
		@Override public ResourceFactory userResources() { return Admin.USER_TEMPLATES; }
		@Override public ResourceFactory anonResources() { return Admin.ANON_TEMPLATES; }
	};

	/** This is the file system containing the default email templates that can be used for
	{@link net.metanotion.email.MailMerge} based implementations of
	{@link net.metanotion.multitenant.adminapp.AdminMailer}.
	*/
	public static final FileSystem<File> DEFAULT_EMAIL_TEMPLATES =
		new ClassLoaderFileSystem(AdminAppFactory.class.getClassLoader(), "/net/metanotion/multitenant/adminapp/assets/emails");

	/** This is a default implementation of the {@link net.metanotion.multitenant.adminapp.AdminMailer} that just logs
	emails via the logging package. */
	public static final AdminMailer LOGGING_ADMIN_MAILER = MailerProxy.mailLogger(AdminMailer.class);

	/** This method provides a backed Iterable instance that enumerates the list of prefixes of currently running
		tenant instances. This method was designed to sastify the constructor requirements of the
		{@link net.metanotion.multitenant.SimpleMultiTenantAppFactory}.
		@param ds The database that stores the list of running instances.
		@return A list of string prefixes identifying the currently running instances.
	*/
	public static Iterable<String> getDefaultInstanceList(final DataSource ds) {
		return new Iterable<String>() {
			@Override public Iterator<String> iterator() {
				logger.debug("Enumerating instances.");
				return JDBCTransaction.doTX(ds, new Block<Connection, Iterable<String>>() {
					@Override public Iterable<String> eval(final Connection conn) throws Exception {
						return tenantQueries.listTenants(conn);
					}
				}).iterator();
			}
		};
	}

	/** This method provides a backed Iterable instance that enumerates the list of prefixes of currently running
		tenant instances. This method was designed to sastify the constructor requirements of the
		{@link net.metanotion.multitenant.SimpleMultiTenantAppFactory}.
		@return A list of string prefixes identifying the currently running instances.
	*/
	public Iterable<String> getDefaultInstanceList() { return getDefaultInstanceList(ds); }

	private final URIPrefixDispatcher adminDispatcher;
	private final MultiTenantAdmin mt;
	private final DataSource ds;
	private final String appPrefix;
	private final AdminMailer mailer;
	private final AuthMailer authMailer;
	private final ResourceFactoryConfiguration resourcesConfig;

	/** Create a new AppFactory instance for the multitenant tenant instance administration panel web application.
		@param mt An implementation of the multitenant tenant instance control interface.
		@param ds The data source for the database connection supplying a schema compatible with the schema provided by
			this classes {@link #schemaFactory} method.
		@param appPrefix the string prefix the single instance of the administrative control panel web app will live at.
		@throws IOException if there is failure in generating the administrative web application dispatcher.
	*/
	public AdminAppFactory(final MultiTenantAdmin mt, final DataSource ds, final String appPrefix) throws IOException {
		this(mt, ds, appPrefix, LOGGING_ADMIN_MAILER, AuthFactory.LOGGING_AUTH_MAILER, DEFAULT_RESOURCES);
	}

	/** Create a new AppFactory instance for the multitenant tenant instance administration panel web application.
		@param mt An implementation of the multitenant tenant instance control interface.
		@param ds The data source for the database connection supplying a schema compatible with the schema provided by
			this classes {@link #schemaFactory} method.
		@param appPrefix the string prefix the single instance of the administrative control panel web app will live at.
		@param resourcesConfig A configuration instance of the interface for provided extending configuration to the
			administrative control panel web app.
		@throws IOException if there is failure in generating the administrative web application dispatcher.
	*/
	public AdminAppFactory(final MultiTenantAdmin mt,
			final DataSource ds,
			final String appPrefix,
			final ResourceFactoryConfiguration resourcesConfig) throws IOException {
		this(mt, ds, appPrefix, LOGGING_ADMIN_MAILER, AuthFactory.LOGGING_AUTH_MAILER, resourcesConfig);
	}

	/** Create a new AppFactory instance for the multitenant tenant instance administration panel web application.
		@param mt An implementation of the multitenant tenant instance control interface.
		@param ds The data source for the database connection supplying a schema compatible with the schema provided by
			this classes {@link #schemaFactory} method.
		@param appPrefix the string prefix the single instance of the administrative control panel web app will live at.
		@param mailer An instance for sending emails generated by instance control events.
		@param authMailer An instance for sending emails generated by user account/authentication emails for the
			administrative control panel user accounts.
		@throws IOException if there is failure in generating the administrative web application dispatcher.
	*/
	public AdminAppFactory(final MultiTenantAdmin mt,
			final DataSource ds,
			final String appPrefix,
			final AdminMailer mailer,
			final AuthMailer authMailer) throws IOException {
		this(mt, ds, appPrefix, mailer, authMailer, DEFAULT_RESOURCES);
	}

	/** Create a new AppFactory instance for the multitenant tenant instance administration panel web application.
		@param mt An implementation of the multitenant tenant instance control interface.
		@param ds The data source for the database connection supplying a schema compatible with the schema provided by
			this classes {@link #schemaFactory} method.
		@param appPrefix the string prefix the single instance of the administrative control panel web app will live at.
		@param mailer An instance for sending emails generated by instance control events.
		@param authMailer An instance for sending emails generated by user account/authentication emails for the
			administrative control panel user accounts.
		@param resourcesConfig A configuration instance of the interface for provided extending configuration to the
			administrative control panel web app.
		@throws IOException if there is failure in generating the administrative web application dispatcher.
	*/
	public AdminAppFactory(final MultiTenantAdmin mt,
			final DataSource ds,
			final String appPrefix,
			final AdminMailer mailer,
			final AuthMailer authMailer,
			final ResourceFactoryConfiguration resourcesConfig) throws IOException {
		this.mt = mt;
		this.ds = ds;
		this.appPrefix = appPrefix;
		this.mailer = mailer;
		this.authMailer = authMailer;
		this.resourcesConfig = resourcesConfig;
		this.adminDispatcher = new URIPrefixDispatcher()
			.addDispatcher(Manager.INSTANCE_API, Manager.instanceApiDispatcher(ds))
			.addDispatcher(Manager.TENANTS_API, Manager.tenantsDispatcher())
			.addDispatcher(Constants.AUTH_PREFIX, FormsAuth.dispatcher())
			.addDispatcher("", new ResourceDispatcher());
	}

	// AppFactory
	@Override public Dispatcher<? extends Object,RequestObject> dispatcher() { return adminDispatcher; }
	@Override public Instance newInstance(final DataSource unused, final String prefix, final Unknown container) {
		return new Admin(this.mt,
			container.lookupInterface(MultiTenantAppFactory.class),
			this.appPrefix,
			prefix,
			this.ds,
			this.resourcesConfig,
			mailer,
			authMailer);
	}

	private static final SchemaGenerator schema = new SchemaGenerator() {
		@Override public Iterator<String> openSchema() {
			return new ChainedIterator<String>(Arrays.asList(
				SQLRealm.schemaFactory().openSchema(),
				SequenceExecutor.openSchema(AdminAppFactory.class, ";--")));
		}
	};

	/** Static interface method used by the {@link net.metanotion.sql.BuildDb} utility.
		@return A schema generator that provides the schema needed by this application.
	*/
	public static SchemaGenerator schemaFactory() { return schema; }

	@Override public Iterator<String> openSchema() { return schema.openSchema(); }



	private static final class Admin implements Instance, AuthStates<Admin,Session<Admin>> {
		private static ResourceFactory getRF(final ResourceFactory factoryValue, final ResourceFactory defaultValue) {
			return (factoryValue != null) && (factoryValue != defaultValue) ?
				new ChainedResourceFactory(factoryValue, defaultValue) :
				defaultValue;
		}

		protected static final ResourceFactory ANON_TEMPLATES =
			new TemplateResources(
				new ClassLoaderFileSystem(Admin.class.getClassLoader(),
					"net/metanotion/multitenant/adminapp/assets/anon/templates"),
				new StaticResources(
					new ClassLoaderFileSystem(Admin.class.getClassLoader(),
						"net/metanotion/multitenant/adminapp/assets/anon/files"),
					new MountPointResources(new StaticResources(JsonUtil.getJavaScript()), "/js")));

		protected static final ResourceFactory USER_TEMPLATES =
			new ChainedResourceFactory(
				new TemplateResources(
					new ClassLoaderFileSystem(Admin.class.getClassLoader(),
						"net/metanotion/multitenant/adminapp/assets/user/templates"),
					new StaticResources(
						new ClassLoaderFileSystem(Admin.class.getClassLoader(),
							"net/metanotion/multitenant/adminapp/assets/user/files"))),
				ANON_TEMPLATES);

		private final AuthFsm<Admin,Session<Admin>> smFactory = new AuthFsm<>(this);

		private final ResourceFactory userResources;
		private final ResourceFactory anonResources;

		private final String prefix;
		private final MultiTenantAdmin mt;
		private final MultiTenantAppFactory container;
		protected final AdminAppFactory.ResourceFactoryConfiguration resourcesConfig;
		protected final FormsAuth formsAuth;
		protected final Manager manager;
		@Service public final DataSource ds;
		@Service public final FormsAuthAPI formsAPI;

		public Admin(final MultiTenantAdmin mt,
				final MultiTenantAppFactory container,
				final String appPrefix,
				final String prefix,
				final DataSource ds,
				final AdminAppFactory.ResourceFactoryConfiguration resourcesConfig,
				final AdminMailer mailer,
				final AuthMailer authMailer) {
			this.mt = mt;
			this.container = container;
			this.prefix = prefix;
			this.ds = ds;
			this.resourcesConfig = resourcesConfig;
			this.userResources = getRF(resourcesConfig.userResources(), USER_TEMPLATES);
			this.anonResources = getRF(resourcesConfig.anonResources(), ANON_TEMPLATES);
			final SQLRealm realm = new SQLRealm(ds);
			final AuthAPI authApi = AuthFactory.instance(ds, realm, authMailer, prefix + Constants.AUTH_PREFIX);
			this.formsAuth = new FormsAuth(prefix + Constants.AUTH_PREFIX, authApi);
			this.formsAPI = formsAuth.instance();
			this.manager = new Manager(prefix, ds, realm, userResources, mt, container, appPrefix, mailer);
		}

		public String authPrefix() { return prefix + Constants.AUTH_PREFIX; }

		// Instance
		@Override public Unknown newSession() { return smFactory.newSession(this); }
		@Override public <I> I lookupInterface(Class<I> theInterface) { throw new UnsupportedOperationException(); }

		// AuthStates
		@Override public Session<Admin> login(final UserToken user, final Session<Admin> currentState) {
			return new User(currentState.appInstance(), user, userResources);
		}

		@Override public Session<Admin> logout(final Session<Admin> currentState) {
			return new Anonymous(currentState.appInstance(), anonResources);
		}

		@Override public Session<Admin> newSession(final Admin a, final StateMachine sm) {
			return new Anonymous(a, anonResources);
		}

		private static final class Anonymous implements Session<Admin> {
			private static final Logger logger = LoggerFactory.getLogger(Anonymous.class);
			private static final UnknownMagic u = new UnknownMagic(Anonymous.class);
			private static final ScriptingObjectMagic som = new ScriptingObjectMagic(Anonymous.class);

			@Extends public final Admin admin;
			@Service public final ObjectServer os;
			@Service public final ResourceFactory resources;
			@Service public final UserToken uid = null;
			@Scriptable("AuthUI") public Unknown formsAuthUI;

			public Anonymous(final Admin admin, final ResourceFactory resources) {
				this.resources = resources;
				this.admin = admin;
				this.formsAuthUI = admin.formsAuth.newSession(this);
				this.os = som.instance(this);
			}

			@Override public <I> I lookupInterface(final Class<I> theInterface) { return u.lookupInterface(theInterface, this); }
			@Override public Admin appInstance() { return this.admin; }
		}

		private static final class User implements Session<Admin> {
			private static final UnknownMagic u = new UnknownMagic(User.class);
			private static final ScriptingObjectMagic som = new ScriptingObjectMagic(User.class);

			@Extends public final Admin admin;
			@Service public final UserToken uid;
			@Service public final ObjectServer os;
			@Service public final ResourceFactory resources;
			@Service public final TenantsApi tenantsApi;
			@Service public final InstanceSingleApi singleApi;
			@Scriptable("AuthUI") public final Unknown formsAuthUI;

			public User(final Admin admin, final UserToken uid, final ResourceFactory resources) {
				this.uid = uid;
				this.resources = resources;
				this.admin = admin;
				this.formsAuthUI = admin.formsAuth.newSession(this);
				this.tenantsApi = admin.manager;
				this.singleApi = admin.manager;
				this.os = som.instance(this);
			}

			@Override public <I> I lookupInterface(final Class<I> theInterface) { return u.lookupInterface(theInterface, this); }
			@Override public Admin appInstance() { return this.admin; }
		}
	}
}