net.metanotion.multitenant.adminapp.Manager.java Source code

Java tutorial

Introduction

Here is the source code for net.metanotion.multitenant.adminapp.Manager.java

Source

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

import java.io.IOException;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;

import org.apache.commons.text.StringEscapeUtils;

import org.joda.time.ReadableInstant;

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

import net.metanotion.authident.AuthPassword;
import net.metanotion.authident.AuthUtils;
import net.metanotion.authident.CredentialedUserToken;
import net.metanotion.authident.UserManagement;
import net.metanotion.authident.UserToken;
import net.metanotion.exportimport.InstanceExport;
import net.metanotion.functor.Block;
import net.metanotion.functor.Predicate;
import net.metanotion.io.ClassLoaderFileSystem;
import net.metanotion.io.File;
import net.metanotion.io.FileSystem;
import net.metanotion.json.JsonArray;
import net.metanotion.json.JsonObject;
import net.metanotion.multitenant.MultiTenantAdmin;
import net.metanotion.multitenant.MultiTenantAppFactory;
import net.metanotion.scripting.ObjectServer;
import net.metanotion.simpletemplate.ResourceFactory;
import net.metanotion.util.Dispatcher;
import net.metanotion.util.JDBCTransaction;
import net.metanotion.util.PredicateDispatcher;
import net.metanotion.util.SecureString;
import net.metanotion.util.Unknown;
import net.metanotion.web.FileUpload;
import net.metanotion.web.HttpStatus;
import net.metanotion.web.HttpValues;
import net.metanotion.web.RequestObject;
import net.metanotion.web.concrete.HttpErrorGenerator;
import net.metanotion.web.concrete.JsonUtil;
import net.metanotion.web.concrete.ObjectPrefixDispatcher;
import net.metanotion.web.concrete.URIPrefixDispatcher;
import net.metanotion.web.concrete.WebInterfaceDispatcher;

/** This class provides an implementation of the HTTP API's for managing tenants. */
public final class Manager implements TenantsApi, InstanceSingleApi {
    private static final Logger logger = LoggerFactory.getLogger(Manager.class);
    private static final Queries tenantQueries = new Queries();
    private static final String INSTANCE_HTML = "/instance/";

    public static final String INSTANCE_UI = "html/";
    public static final String ADMIN_LOG_API = "adminLog/";
    public static final String ADMINS_API = "admins/";
    public static final String EXPORT_API = "exports/";
    public static final String USERS_API = "users/";

    public static final String TENANTS_API = "/tenants/";
    public static final String INSTANCE_API = "/api/";

    private static final String SLASH = "/";
    private static final String SUCCESS = "{ \"success\": true }";

    /** This file system exposes the JS resources for administrative web application UI components. */
    public static final FileSystem<File> ADMIN_JS = new ClassLoaderFileSystem(Manager.class.getClassLoader(),
            "/net/metanotion/multitenant/adminapp/assets/user/files/js");

    /** Generate the dispatcher for the tenant management API.
       @return Dispatcher for the {@link net.metanotion.multitenant.adminapp.TenantsApi}.
       @throws IOException if the dispatcher cannot be generated.
    */
    public static Dispatcher<? extends Object, RequestObject> tenantsDispatcher() throws IOException {
        return Constants.ERRORS.dispatcher(new WebInterfaceDispatcher<>(TenantsApi.class));
    }

    /** Wrap a per tenant instance dispatcher with an authentication check and tenant ID prefix dispatcher.
       @param ds The data source administrative database for the tenant user authentication check.
       @param disp The prefix dispatcher for the various per-tenant instance feature management API's.
       @return A wrapped prefix dispatcher.
    */
    public static Dispatcher<? extends Object, RequestObject> wrapInstanceDispatcher(final DataSource ds,
            final URIPrefixDispatcher disp) {
        return new ObjectPrefixDispatcher<Object>(new PredicateDispatcher<Object, RequestObject, Exception>(
                Object.class, disp, Manager.tenantUserPermissionPredicate(ds)), Constants.TENANT_ID);
    }

    /** Generate the entire dispatcher for the standard per tenant instance API's supported by the Manager
    implementation.
       @param ds The data source administrative database for the tenant user authentication check.
       @return The dispatcher for the per tenant instance API's implemented by
     {@link net.metanotion.multitenant.adminapp.InstanceSingleApi} and including the JSON api exposed at URI
     fragment: "[tenant id]/html/singleApi".
    */
    public static Dispatcher<? extends Object, RequestObject> instanceApiDispatcher(final DataSource ds) {
        try {
            return Constants.ERRORS.dispatcher(wrapInstanceDispatcher(ds, new URIPrefixDispatcher()
                    .addDispatcher(INSTANCE_UI, new WebInterfaceDispatcher<>(InstanceSingleApi.class))
                    .addDispatcher(ADMINS_API, adminsDispatcher()).addDispatcher(EXPORT_API, exportDispatcher())
                    .addDispatcher(USERS_API, usersDispatcher())
                    .addDispatcher(ADMIN_LOG_API, adminLogDispatcher())));
        } catch (final IOException ioe) {
            throw new RuntimeException(ioe);
        }
    }

    /** Generate the dispatcher for the per instance tenant management web page.
       @return Dispatcher for the {@link net.metanotion.multitenant.adminapp.InstanceApi}.
       @throws IOException if the dispatcher cannot be generated.
    */
    public static Dispatcher<Unknown, RequestObject> instanceDispatcher() throws IOException {
        return new WebInterfaceDispatcher<>(InstanceApi.class);
    }

    /** Generate the dispatcher for the per tenant instance administrator management api.
       @return Dispatcher for the {@link net.metanotion.multitenant.adminapp.InstanceAdminsApi}.
       @throws IOException if the dispatcher cannot be generated.
    */
    public static Dispatcher<Unknown, RequestObject> adminsDispatcher() throws IOException {
        return new WebInterfaceDispatcher<>(InstanceAdminsApi.class);
    }

    /** Generate the dispatcher for the per tenant instance export management api.
       @return Dispatcher for the {@link net.metanotion.multitenant.adminapp.InstanceExportApi}.
       @throws IOException if the dispatcher cannot be generated.
    */
    public static Dispatcher<Unknown, RequestObject> exportDispatcher() throws IOException {
        return new WebInterfaceDispatcher<>(InstanceExportApi.class);
    }

    /** Generate the dispatcher for the per tenant instance users management api.
       @return Dispatcher for the {@link net.metanotion.multitenant.adminapp.InstanceUsersApi}.
       @throws IOException if the dispatcher cannot be generated.
    */
    public static Dispatcher<Unknown, RequestObject> usersDispatcher() throws IOException {
        return new WebInterfaceDispatcher<>(InstanceUsersApi.class);
    }

    /** Generate the dispatcher for the per tenant instance administrative log management api.
       @return Dispatcher for the {@link net.metanotion.multitenant.adminapp.InstanceAdminLogApi}.
       @throws IOException if the dispatcher cannot be generated.
    */
    public static Dispatcher<Unknown, RequestObject> adminLogDispatcher() throws IOException {
        return new WebInterfaceDispatcher<>(InstanceAdminLogApi.class);
    }

    private final DataSource adminDS;
    private final AuthPassword pw;
    private final ResourceFactory resources;
    private final Object tenantsApi;
    private final MultiTenantAdmin mt;
    private final MultiTenantAppFactory container;
    private final String appPrefix;
    private final String prefix;
    private final AdminMailer mailer;

    /** Create a new instance of the Manager implementation.
       @param prefix The prefix the web app serving up the Manager implementation lives at.
       @param adminDS The data source for the administrative control panel web app schema.
       @param pw The authentication API provider instance for the admin web app user database.
       @param resources The resource factory for the user authenticated assets.
       @param mt The implementation of the tenant instance control interface to use.
       @param container The multitenant tenant container implementation.
       @param appPrefix The prefix common to tenant instance. Tenant instance URI's prefixes are generated by
     concatenating the Tenant database to the appPrefix.
       @param mailer The mailer implementation for sending tenant administration event emails.
    */
    public Manager(final String prefix, final DataSource adminDS, final AuthPassword pw,
            final ResourceFactory resources, final MultiTenantAdmin mt, final MultiTenantAppFactory container,
            final String appPrefix, final AdminMailer mailer) {
        this.adminDS = adminDS;
        this.pw = pw;
        this.resources = resources;
        this.mt = mt;
        this.container = container;
        this.appPrefix = appPrefix;
        this.prefix = prefix;
        this.mailer = mailer;
        this.tenantsApi = JsonUtil.makeWebApi(TenantsApi.class, prefix + TENANTS_API);
    }

    /** Inject the error generator handlers into the HttpErrorGenerator instance so that exceptions thrown by the
    Manager class will be properly translated into error responses.
       @param errors The error generator instance to inject the definitions into.
       @throws IOException if there was an error loading the definitions from the class loader.
    */
    public static void addErrors(final HttpErrorGenerator errors) throws IOException {
        errors.load(Queries.class);
        errors.load(Manager.class);
        errors.setDefaultError(".error_default", "error_unspecified", HttpStatus.SERVER_ERROR.codeNumber());
    }

    /** This method generates the appropriate predicate to be used by a
    {@link net.metanotion.web.concrete.ObjectPrefixDispatcher} to verify whether an HTTP request against a tenant
    instance by the currently authenticated user is allowed based on the user's status as either a tenant admin or
    tenant owner. This predicated does NOT verify specific ooperations permitted only to owners or verify that the user
    has provided their password for operations that require a password to initiate.
       @param adminDS the data source for the administrative web application database.
       @return The predicate to use with the {@link net.metanotion.web.concrete.ObjectPrefixDispatcher}
    */
    public static Predicate<Map.Entry<Object, RequestObject>, Exception> tenantUserPermissionPredicate(
            final DataSource adminDS) {
        return new Predicate<Map.Entry<Object, RequestObject>, Exception>() {
            @Override
            public void eval(final Map.Entry<Object, RequestObject> requestInfo) throws Exception {
                logger.debug("auth check on tid {} for uid {}", requestInfo.getKey(), requestInfo.getValue());
                final long tid = Long.parseLong(requestInfo.getValue().get(Constants.TENANT_ID).toString());
                final UserToken user = ((Unknown) requestInfo.getKey()).lookupInterface(UserToken.class);
                try (final Connection conn = adminDS.getConnection()) {
                    if (tenantQueries.checkAuth(conn, user.getID(), tid).size() == 0) {
                        throw new SecurityException("Invalid tenant for user.");
                    }
                }
            }
        };
    }

    // TenantsApi
    @Override
    public Object api() {
        return tenantsApi;
    }

    @Override
    public TenantsApi.InstanceList listInstances(final UserToken uid) throws Exception {
        try (final Connection conn = adminDS.getConnection()) {
            return new TenantsApi.InstanceList(tenantQueries.listInstances(conn, uid.getID()));
        }
    }

    @Override
    public InstanceInfo createInstance(final UserToken uid, final String title) throws Exception {
        final InstanceInfo info = JDBCTransaction.doTX(adminDS, new Block<Connection, InstanceInfo>() {
            @Override
            public InstanceInfo eval(final Connection conn) throws Exception {
                final long tid = tenantQueries.reserveTenantId(conn);
                final String instancePrefix = appPrefix + Long.toString(tid);
                tenantQueries.addTenant(conn, tid, uid.getID(), title, instancePrefix);
                return tenantQueries.getInstance(conn, uid.getID(), tid).get(0);
            }
        });
        mt.createInstance(info.InstancePrefix, container);
        try {
            mailer.createdInstance(AuthUtils.lookupUsername(pw, uid), info.InstancePrefix);
        } catch (final Exception ex) {
            logger.error("{}", ex);
        }
        return info;
    }

    public static void checkAuth(final AuthPassword pw, final UserToken uid, final SecureString password) {
        if (!AuthUtils.checkAuthenticatedIdentityMatches(pw, uid, AuthUtils.lookupUsername(pw, uid), password)) {
            throw new RuntimeException("Authentication failed");
        }
    }

    @Override
    public Object removeInstance(final UserToken uid, final SecureString password, final long tenantId)
            throws Exception {
        checkAuth(pw, uid, password);
        final String instancePrefix = getInstancePrefix(tenantId);
        final Integer i = JDBCTransaction.doTX(adminDS, new Block<Connection, Integer>() {
            @Override
            public Integer eval(final Connection conn) throws Exception {
                return tenantQueries.removeTenant(conn, tenantId, uid.getID());
            }
        });
        if (i.intValue() > 0) {
            mt.removeInstance(instancePrefix, container);
            try {
                mailer.removedInstance(AuthUtils.lookupUsername(pw, uid), instancePrefix);
            } catch (final Exception ex) {
                logger.error("{}", ex);
            }
            return SUCCESS;
        } else {
            throw new RuntimeException();
        }
    }

    @Override
    public InstanceList reassignOwner(final UserToken uid, final SecureString password, final long tenantId,
            final String newOwnerEmail) throws Exception {
        checkAuth(pw, uid, password);
        final String instancePrefix = getInstancePrefix(tenantId);
        return JDBCTransaction.doTX(adminDS, new Block<Connection, InstanceList>() {
            @Override
            public InstanceList eval(final Connection conn) throws Exception {
                if (tenantQueries.reassignOwner(conn, tenantId, uid.getID(), newOwnerEmail) > 0) {
                    try {
                        mailer.setAsOwner(newOwnerEmail, instancePrefix);
                    } catch (final Exception ex) {
                        logger.error("{}", ex);
                    }
                    try {
                        mailer.reassignedOwnership(AuthUtils.lookupUsername(pw, uid), instancePrefix);
                    } catch (final Exception ex) {
                        logger.error("{}", ex);
                    }
                    try {
                        tenantQueries.logEvent(conn, tenantId, uid.getID(), AuthUtils.lookupUsername(pw, uid),
                                "{ \"action\": \"reassignOwner\", \"data\": { \"newOwner\": [ \""
                                        + StringEscapeUtils.escapeJson(newOwnerEmail) + "\" ] }}");
                    } catch (final Exception ex) {
                        logger.error("{}", ex);
                    }
                    return new InstanceList(tenantQueries.getInstance(conn, uid.getID(), tenantId));
                } else {
                    final long ownerId = tenantQueries.getOwner(conn, tenantId);
                    if (ownerId != uid.getID()) {
                        throw new RuntimeException("Invalid owner.");
                    } else if (pw.getIdentity(newOwnerEmail) == null) {
                        throw new RuntimeException("No such user.");
                    } else {
                        throw new RuntimeException();
                    }
                }
            }
        });
    }

    @Override
    public InstanceInfo updateTitle(final UserToken uid, final long tenantId, final String title) throws Exception {
        return JDBCTransaction.doTX(adminDS, new Block<Connection, InstanceInfo>() {
            @Override
            public InstanceInfo eval(final Connection conn) throws Exception {
                if (tenantQueries.updateTitle(conn, tenantId, uid.getID(), title) > 0) {
                    return tenantQueries.getInstance(conn, uid.getID(), tenantId).get(0);
                } else {
                    throw new RuntimeException();
                }
            }
        });
    }

    @Override
    public InstanceInfo stopInstance(final UserToken uid, final long tenantId) throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final Integer i = JDBCTransaction.doTX(adminDS, new Block<Connection, Integer>() {
            @Override
            public Integer eval(final Connection conn) throws Exception {
                return tenantQueries.stopTenant(conn, tenantId);
            }
        });
        if (i.intValue() > 0) {
            mt.stopInstance(instancePrefix);
            try (final Connection conn = adminDS.getConnection()) {
                tenantQueries.logEvent(conn, tenantId, uid.getID(), AuthUtils.lookupUsername(pw, uid),
                        "{ \"action\": \"instanceStopped\", \"data\": { }}");
                return tenantQueries.getInstance(conn, uid.getID(), tenantId).get(0);
            }
        } else {
            throw new RuntimeException();
        }
    }

    @Override
    public InstanceInfo startInstance(final UserToken uid, final long tenantId) throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final Integer i = JDBCTransaction.doTX(adminDS, new Block<Connection, Integer>() {
            @Override
            public Integer eval(final Connection conn) throws Exception {
                return tenantQueries.startTenant(conn, tenantId);
            }
        });
        if (i.intValue() > 0) {
            mt.startInstance(instancePrefix, container);
            try (final Connection conn = adminDS.getConnection()) {
                tenantQueries.logEvent(conn, tenantId, uid.getID(), AuthUtils.lookupUsername(pw, uid),
                        "{ \"action\": \"instanceStarted\", \"data\": { }}");
                return tenantQueries.getInstance(conn, uid.getID(), tenantId).get(0);
            }
        } else {
            throw new RuntimeException();
        }
    }

    // InstanceSingleApi
    @Override
    public JsonObject singleApi(final long tenantId) throws Exception {
        final JsonObject ret = this.adminsApi(tenantId);
        final JsonObject exports = this.exportsApi(tenantId);
        for (final String key : exports.keySet()) {
            ret.put(key, exports.get(key));
        }
        final JsonObject users = this.usersApi(tenantId);
        for (final String key : users.keySet()) {
            ret.put(key, users.get(key));
        }
        final JsonObject adminLog = this.adminLogApi(tenantId);
        for (final String key : adminLog.keySet()) {
            ret.put(key, adminLog.get(key));
        }
        return ret;
    }

    // InstanceAdminsApi
    @Override
    public JsonObject adminsApi(final long tenantId) {
        return JsonUtil.makeWebApi(InstanceAdminsApi.class, prefix + INSTANCE_API + tenantId + SLASH + ADMINS_API);
    }

    @Override
    public InstanceAdminsApi.AdminList listAdmins(final Connection conn, final long tenantId) throws Exception {
        return new InstanceAdminsApi.AdminList(tenantQueries.listTenantAdmins(conn, tenantId));
    }

    @Override
    public AdminAccount addAdmin(final Connection conn, final long tenantId, final UserToken uid,
            final SecureString password, final String email) throws Exception {
        checkAuth(pw, uid, password);
        final String instancePrefix = getInstancePrefix(tenantId);
        if (tenantQueries.addAdmin(conn, tenantId, email) > 0) {
            try {
                mailer.addAdmin(email, instancePrefix);
            } catch (final Exception ex) {
                logger.error("{}", ex);
            }
            try {
                tenantQueries.logEvent(conn, tenantId, uid.getID(), AuthUtils.lookupUsername(pw, uid),
                        "{ \"action\": \"addAdministrator\", \"data\": { \"administrators\": [ \""
                                + StringEscapeUtils.escapeJson(email) + "\" ] }}");
            } catch (final Exception ex) {
                logger.error("{}", ex);
            }
            return tenantQueries.getTenantAdmin(conn, tenantId, email).get(0);
        } else {
            throw new RuntimeException();
        }
    }

    @Override
    public Object removeAdmin(final Connection conn, final long tenantId, final UserToken uid,
            final SecureString password, final String email) throws Exception {
        checkAuth(pw, uid, password);
        final String instancePrefix = getInstancePrefix(tenantId);
        if (uid instanceof CredentialedUserToken) {
            if (email.equals(((CredentialedUserToken) uid).getCredential().toString())) {
                logger.debug("Can't remove yourself as an admin");
                // TO DO a better choice in exceptions.
                throw new RuntimeException();
            }
        }
        if (tenantQueries.deleteAdmin(conn, tenantId, email) > 0) {
            try {
                mailer.removeAdmin(email, instancePrefix);
            } catch (final Exception ex) {
                logger.error("{}", ex);
            }
            try {
                tenantQueries.logEvent(conn, tenantId, uid.getID(), AuthUtils.lookupUsername(pw, uid),
                        "{ \"action\": \"removeAdministrator\", \"data\": { \"administrators\": [ \""
                                + StringEscapeUtils.escapeJson(email) + "\" ] }}");
            } catch (final Exception ex) {
                logger.error("{}", ex);
            }
            return SUCCESS;
        } else {
            throw new RuntimeException();
        }
    }

    // InstanceUsersApi
    public String getInstancePrefix(final long tenantId) throws Exception {
        try (final Connection conn = adminDS.getConnection()) {
            return tenantQueries.getPrefix(conn, tenantId);
        }
    }

    @Override
    public JsonObject usersApi(final long tenantId) throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final UserManagement um = mt.getInstance(instancePrefix).lookupInterface(UserManagement.class);
        final JsonObject api = JsonUtil.makeWebApi(InstanceUsersApi.class,
                prefix + INSTANCE_API + tenantId + SLASH + USERS_API);
        final JsonArray roleList = new JsonArray();
        for (final String role : um.listRoles()) {
            roleList.add(role);
        }
        api.put("roles", roleList);
        return api;
    }

    @Override
    public InstanceUsersApi.UserList listUsers(final long tenantId, final int count, final int offset)
            throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final UserManagement um = mt.getInstance(instancePrefix).lookupInterface(UserManagement.class);
        return new InstanceUsersApi.UserList(um.getAccounts(offset, count));
    }

    @Override
    public Object addUser(final long tenantId, final UserToken uid, final String username) throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final UserManagement um = mt.getInstance(instancePrefix).lookupInterface(UserManagement.class);
        logger.debug("Tenant: {} add user {}", tenantId, username);
        final ArrayList<String> accts = new ArrayList<>();
        accts.add(username);
        if (um.addAccounts(accts).size() == 0) {
            try (final Connection conn = adminDS.getConnection()) {
                tenantQueries.logEvent(conn, tenantId, uid.getID(), AuthUtils.lookupUsername(pw, uid),
                        "{ \"action\": \"addUser\", \"data\": { \"users\": [ \""
                                + StringEscapeUtils.escapeJson(username) + "\" ] }}");
            } catch (final Exception ex) {
                logger.error("{}", ex);
            }
            return SUCCESS;
        } else {
            throw new RuntimeException();
        }
    }

    @Override
    public Object updateUserEmails(final long tenantId, final UserToken uid, final UserManagement.Account set)
            throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final UserManagement um = mt.getInstance(instancePrefix).lookupInterface(UserManagement.class);
        um.setUsernames(set.userId, set.emails);
        try (final Connection conn = adminDS.getConnection()) {
            tenantQueries.logEvent(conn, tenantId, uid.getID(), AuthUtils.lookupUsername(pw, uid),
                    "{ \"action\": \"updateUserEmails\", \"data\": { \"userId\": " + Long.toString(set.userId)
                            + " }}");
        } catch (final Exception ex) {
            logger.error("{}", ex);
        }
        return SUCCESS;
    }

    @Override
    public Object updateUserRoles(final long tenantId, final UserToken uid, final UserManagement.Account set)
            throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final UserManagement um = mt.getInstance(instancePrefix).lookupInterface(UserManagement.class);
        um.setRoles(set.userId, set.roles);
        try (final Connection conn = adminDS.getConnection()) {
            tenantQueries.logEvent(conn, tenantId, uid.getID(), AuthUtils.lookupUsername(pw, uid),
                    "{ \"action\": \"updateUserRoles\", \"data\": { \"userId\": " + Long.toString(set.userId)
                            + " }}");
        } catch (final Exception ex) {
            logger.error("{}", ex);
        }
        return SUCCESS;
    }

    @Override
    public Object removeUser(final long tenantId, final UserToken uid, final long userId) throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final UserManagement um = mt.getInstance(instancePrefix).lookupInterface(UserManagement.class);
        um.deleteAccount(userId);
        try (final Connection conn = adminDS.getConnection()) {
            tenantQueries.logEvent(conn, tenantId, uid.getID(), AuthUtils.lookupUsername(pw, uid),
                    "{ \"action\": \"removeUser\", \"data\": { \"userId\": " + Long.toString(userId) + " }}");
        } catch (final Exception ex) {
            logger.error("{}", ex);
        }
        return SUCCESS;
    }

    @Override
    public InstanceUsersApi.AccountList importUsers(final long tenantId, final UserToken uid,
            final InstanceUsersApi.AccountList emails) throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final UserManagement um = mt.getInstance(instancePrefix).lookupInterface(UserManagement.class);
        try (final Connection conn = adminDS.getConnection()) {
            tenantQueries.logEvent(conn, tenantId, uid.getID(), AuthUtils.lookupUsername(pw, uid),
                    "{ \"action\": \"importUsers\", \"data\": {}}");
        } catch (final Exception ex) {
            logger.error("{}", ex);
        }
        return new InstanceUsersApi.AccountList(um.addAccounts(emails.accounts));
    }

    // InstanceExportApi
    @Override
    public JsonObject exportsApi(final long tenantId) {
        return JsonUtil.makeWebApi(InstanceExportApi.class, prefix + INSTANCE_API + tenantId + SLASH + EXPORT_API);
    }

    @Override
    public InstanceExportApi.EntryList listExports(final long tenantId, final int count, final int offset)
            throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final InstanceExport ie = mt.getInstance(instancePrefix).lookupInterface(InstanceExport.class);
        return new InstanceExportApi.EntryList(ie.listExports(offset, count));
    }

    @Override
    public InstanceExport.Status exportStatusPoll(final long tenantId) throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final InstanceExport ie = mt.getInstance(instancePrefix).lookupInterface(InstanceExport.class);
        return ie.status();
    }

    @Override
    public InstanceExport.Status startExport(final long tenantId) throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final InstanceExport ie = mt.getInstance(instancePrefix).lookupInterface(InstanceExport.class);
        return ie.initiateExport();
    }

    @Override
    public InstanceExport.Status resetInstance(final long tenantId, final UserToken uid,
            final SecureString password) throws Exception {
        checkAuth(pw, uid, password);
        final String instancePrefix = getInstancePrefix(tenantId);
        final InstanceExport ie = mt.getInstance(instancePrefix).lookupInterface(InstanceExport.class);
        mailer.resetInstance(AuthUtils.lookupUsername(pw, uid), instancePrefix);
        try (final Connection conn = adminDS.getConnection()) {
            final long ownerId = tenantQueries.getOwner(conn, tenantId);
            if (ownerId != uid.getID()) {
                mailer.resetInstance(tenantQueries.getOwnerEmail(conn, ownerId), instancePrefix);
            }
            try {
                tenantQueries.logEvent(conn, tenantId, uid.getID(), AuthUtils.lookupUsername(pw, uid),
                        "{ \"action\": \"resetInstance\", \"data\": {}}");
            } catch (final Exception ex) {
                logger.error("{}", ex);
            }
        }
        return ie.resetInstance();
    }

    @Override
    public String uploadExport(final long tenantId, final String responseTag, final FileUpload file)
            throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final InstanceExport ie = mt.getInstance(instancePrefix).lookupInterface(InstanceExport.class);
        ie.storeExternalExport(file.getClientFilename(), file.getMIMEType(), file.getInputStream());
        return "<!DOCTYPE html><html><head><meta charset=\"UTF-8\">" + "<script>window.parent.iframeLoadDequeue('"
                + responseTag + "', { });</script>" + "</head><body></body></html>";
    }

    @Override
    public HttpValues getExport(final long tenantId, final long exportId) throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final InstanceExport ie = mt.getInstance(instancePrefix).lookupInterface(InstanceExport.class);
        return ie.getExport(exportId);
    }

    private static final int MAX_RECORDS = 100;

    private static final InstanceExport.Entry findFile(final InstanceExport ie, final long exportId) {
        List<InstanceExport.Entry> entries = ie.listExports(0, MAX_RECORDS);
        int offset = 0;
        while (entries.size() > 0) {
            for (final InstanceExport.Entry e : entries) {
                if (e.fileId == exportId) {
                    return e;
                }
                offset = e.offset;
            }
            entries = ie.listExports(offset + 1, MAX_RECORDS);
        }
        throw new RuntimeException("Export not found");
    }

    @Override
    public InstanceExport.Status startImport(final long tenantId, final UserToken uid, final SecureString password,
            final long exportId) throws Exception {
        checkAuth(pw, uid, password);
        final String instancePrefix = getInstancePrefix(tenantId);
        final InstanceExport ie = mt.getInstance(instancePrefix).lookupInterface(InstanceExport.class);
        mailer.restoreInstanceFromExport(AuthUtils.lookupUsername(pw, uid), instancePrefix);
        try (final Connection conn = adminDS.getConnection()) {
            final long ownerId = tenantQueries.getOwner(conn, tenantId);
            if (ownerId != uid.getID()) {
                mailer.restoreInstanceFromExport(tenantQueries.getOwnerEmail(conn, ownerId), instancePrefix);
            }
            try {
                final InstanceExport.Entry e = findFile(ie, exportId);
                tenantQueries.logEvent(conn, tenantId, uid.getID(), AuthUtils.lookupUsername(pw, uid),
                        "{ \"action\": \"importInstance\", \"data\": { \"exportId\": " + Long.toString(exportId)
                                + ", \"exportFile\": \"" + StringEscapeUtils.escapeJson(e.filename) + "\""
                                + ", \"exportDesc\": \"" + StringEscapeUtils.escapeJson(e.description) + "\" }}");
            } catch (final Exception ex) {
                logger.error("{}", ex);
            }
        }
        return ie.initiateImport(exportId);
    }

    @Override
    public Object removeExport(final long tenantId, final long exportId) throws Exception {
        final String instancePrefix = getInstancePrefix(tenantId);
        final InstanceExport ie = mt.getInstance(instancePrefix).lookupInterface(InstanceExport.class);
        ie.deleteExport(exportId);
        return SUCCESS;
    }

    // InstanceAdminLogApi
    @Override
    public JsonObject adminLogApi(final long tenantId) {
        return JsonUtil.makeWebApi(InstanceAdminLogApi.class,
                prefix + INSTANCE_API + tenantId + SLASH + ADMIN_LOG_API);
    }

    @Override
    public InstanceAdminLogApi.LogList searchLogs(final long tenantId, final long startEventId, final int count,
            final String email, final ReadableInstant startDate, final ReadableInstant endDate) throws Exception {
        try (final Connection conn = adminDS.getConnection()) {
            return new InstanceAdminLogApi.LogList(tenantQueries.tenantLogs(conn, tenantId, startEventId, count,
                    startDate, endDate, email == null ? "" : email));
        }
    }

    // InstanceApi
    @Override
    public Object get(final ObjectServer os, final RequestObject ro) {
        return this.resources.get(INSTANCE_HTML + ro.getResource()).skin(os, ro);
    }
}