com.dbmojo.DBMojoServer.java Source code

Java tutorial

Introduction

Here is the source code for com.dbmojo.DBMojoServer.java

Source

package com.dbmojo;

/*
Copyright (C) 2010 Nick Crafford <nickcrafford@gmail.com>
    
This file is part of dbmojo
    
dbmojo is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
    
dbmojo 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 General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with dbmojo.  If not, see <http://www.gnu.org/licenses/>.
*/

import java.io.IOException;
import java.io.File;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Properties;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import java.lang.ClassLoader;

/** A threaded HTTP 1.0 server implemented on top of NanoHTTPD. <br><br>
 *  The default config file location is <i>config.json</i>
 */

public class DBMojoServer extends NanoHTTPD {

    private static String defaultConfigPath = "config.json";

    private boolean useGzip;
    private short serverPort;
    private short maxConcurrentRequests;
    private ConcurrentHashMap<String, ConnectionPool> dbPools;

    private DBMojoServer(boolean useGzip, short serverPort, short maxConcReq,
            ConcurrentHashMap<String, ConnectionPool> dbPools) {
        this.useGzip = useGzip;
        this.serverPort = serverPort;
        this.dbPools = dbPools;
        this.maxConcurrentRequests = maxConcReq;
    }

    private void start() throws IOException {
        super.start(this.serverPort, this.maxConcurrentRequests);
    }

    /** Consume the HTTP request, delegate to the QueryExecutor, and 
     *  return the HTTP response. <br<br>
     *  Each request POST/GET request should contain the following params:<br>
     *  <ul>
     *    <li><strong>alias</strong> - The database to connect to</li>
     *    <li><strong>json</strong> - The json containing the query/update set to 
     *    execute</li>   
     * </ul>
     * Optional params:
     * <ul>
     *    <li><strong>update</strong> - Does the query set contain updates? (Y/N).
     *        If not specified the query set will be executed as queries, 
     *        not updates.</li>
     *    <li><strong>cache</strong> - The time (in seconds) to set the 
     *    'Cache-Control' header to. 
     *        If not specified the header will be set to 'no-cache'</li>
     * </ul>   
     */
    public Response serve(String clientIp, String uri, String method, Properties header, Properties parms) {

        final String json = parms.getProperty("json");
        final boolean update = Util.getBoolean(parms.getProperty("update"));
        final boolean documentFormat = false;
        final int cache = update ? 0 : Util.getInt(parms.getProperty("cache"));
        String alias = parms.getProperty("alias");

        //Log each access attempt
        if (AccessLog.enabled) {
            AccessLog.add(clientIp, method, useGzip, uri, alias, update, json);
        }

        //Catch malformed request errors
        if (json == null || alias == null) {
            final String err = "Malformed request";
            if (DebugLog.enabled)
                DebugLog.add(this, err);
            return respond(Util.getErrorJson(err).toString(), 0);
        }

        try {
            return respond(executeStatement(update, alias, json), cache);
        } catch (Exception e) {
            final String err = e.toString();
            if (DebugLog.enabled)
                DebugLog.add(this, err);
            return respond(Util.getErrorJson(err).toString(), 0);
        }
    }

    private String executeStatement(boolean update, String alias, String json) throws Exception {

        ConnectionPool pool = dbPools.get(alias);

        //Catch incorrect aliases
        if (pool == null) {
            throw new DBMojoServerException("Alias '" + alias + "' is missing");
        }

        QueryExecutor ex = new QueryExecutor(pool);
        String results = ex.execute(json, update);

        if (useGzip) {
            results = Util.gzipString(results);
        }

        return results;
    }

    private Response respond(String text, int cache) {
        NanoHTTPD.Response resp = new NanoHTTPD.Response(HTTP_OK, MIME_PLAINTEXT, text);

        if (useGzip) {
            resp.addHeader("Content-Encoding", "gzip");
        }

        //Cache time is usefull for the web accelerators that might front 
        //DBMojo such as Varnish, Squid, or Oracle WebCache
        if (cache > 0) {
            resp.addHeader("Cache-Control", "public, max-age=" + cache);
        } else {
            resp.addHeader("Cache-Control", "no-cache");
        }

        resp.addHeader("Content-Length", text.length() + "");

        return resp;
    }

    //Parse the provided config file json and return a ready to start 
    //DBMojoServer object
    private static DBMojoServer getMojoServerFromConfig(String[] args) {

        DBMojoServer server = null;

        try {
            String configFilePath = null;
            String json = null;
            JSONObject jObj = null;

            parseJson: {
                //If a command line argument is passed then assume it is the config file.
                //Otherwise use the default location
                if (args.length > 0) {
                    configFilePath = args[0];
                } else {
                    configFilePath = DBMojoServer.defaultConfigPath;
                }

                try {
                    json = Util.fileToString(configFilePath);
                } catch (Exception fileEx) {
                    throw new Exception(
                            "the specified config file, '" + configFilePath + "', could not be found and/or read");
                }

                if (json == null || json.equals("")) {
                    throw new Exception("the specified config file, '" + configFilePath + "', is empty");
                }

                try {
                    jObj = new JSONObject(json);
                } catch (Exception je) {
                    throw new Exception(
                            "the specified config file, '" + configFilePath + "', does not contain valid JSON");
                }
            }

            //Load basic config data
            short serverPort = (short) jObj.optInt("serverPort");
            boolean useGzip = jObj.optBoolean("useGzip");
            short maxConcReq = (short) jObj.optInt("maxConcurrentRequests");
            String accessLogPath = jObj.optString("accessLogPath");
            String errorLogPath = jObj.optString("errorLogPath");
            String debugLogPath = jObj.optString("debugLogPath");

            checkMaxConcurrentReqeusts: {
                if (maxConcReq <= 0) {
                    throw new Exception("please set the max concurrent requests to " + "a resonable number");
                }
            }

            checkServerPort: {
                //Make sure serverPort was specified
                if (serverPort <= 0) {
                    throw new Exception("the server port was not specified");
                }

                //Make sure serverPort is not in use
                ServerSocket tSocket = null;
                try {
                    tSocket = new ServerSocket(serverPort);
                } catch (Exception se) {
                    tSocket = null;
                    throw new Exception("the server port specified is already in use");
                } finally {
                    if (tSocket != null) {
                        tSocket.close();
                    }
                    tSocket = null;
                }
            }

            startLogs: {
                if (!accessLogPath.equals("")) {
                    //Make sure accessLogPath exists
                    Util.pathExists(accessLogPath, true);
                    //Start logging
                    AccessLog.start(accessLogPath);
                }

                if (!errorLogPath.equals("")) {
                    //Make sure errorLogPath exists
                    Util.pathExists(errorLogPath, true);
                    //Start logging
                    ErrorLog.start(errorLogPath);
                }

                if (!debugLogPath.equals("")) {
                    //Make sure debugLogPath exists
                    Util.pathExists(debugLogPath, true);
                    //Start logging
                    DebugLog.start(debugLogPath);
                }
            }

            ConcurrentHashMap<String, ConnectionPool> dbPools = new ConcurrentHashMap<String, ConnectionPool>();
            loadDbAlaises: {
                ClassLoader classLoader = ClassLoader.getSystemClassLoader();
                final JSONArray dbAliases = jObj.getJSONArray("dbAliases");

                for (int i = 0; i < dbAliases.length(); i++) {
                    final JSONObject tObj = dbAliases.getJSONObject(i);
                    final String tAlias = tObj.getString("alias");
                    final String tDriver = tObj.getString("driver");
                    final String tDsn = tObj.getString("dsn");
                    final String tUsername = tObj.getString("username");
                    final String tPassword = tObj.getString("password");
                    int tMaxConnections = tObj.getInt("maxConnections");
                    //Seconds
                    int tExpirationTime = tObj.getInt("expirationTime") * 1000;
                    //Seconds
                    int tConnectTimeout = tObj.getInt("connectTimeout");

                    //Make sure each alias is named
                    if (tAlias.equals("")) {
                        throw new Exception("alias #" + i + " is missing a name");
                    }

                    //Attempt to load each JDBC driver to ensure they are on the class path
                    try {
                        Class aClass = classLoader.loadClass(tDriver);
                    } catch (ClassNotFoundException cnf) {
                        throw new Exception("JDBC Driver '" + tDriver + "' is not on the class path");
                    }

                    //Make sure each alias has a JDBC connection string
                    if (tDsn.equals("")) {
                        throw new Exception("JDBC URL, 'dsn', is missing for alias '" + tAlias + "'");
                    }

                    //Attempt to create a JDBC Connection
                    ConnectionPool tPool;
                    try {
                        tPool = new JDBCConnectionPool(tDriver, tDsn, tUsername, tPassword, 1, 1, 1, tAlias);
                        tPool.checkOut(false);
                    } catch (Exception e) {
                        throw new Exception(
                                "JDBC Connection cannot be established " + "for database '" + tAlias + "'");
                    } finally {
                        tPool = null;
                    }

                    //If the max connections option is not set for this alias 
                    //then set it to 25
                    if (tMaxConnections <= 0) {
                        tMaxConnections = 25;
                        System.out.println("DBMojoServer: Warning, 'maxConnections' " + "not set for alias '"
                                + tAlias + "' using 25");
                    }

                    //If the connection expiration time is not set for this alias then 
                    //set it to 30 seconds
                    if (tExpirationTime <= 0) {
                        tExpirationTime = 30;
                        System.out.println("DBMojoServer: Warning, 'expirationTime' not " + "set for alias '"
                                + tAlias + "' using 30 seconds");
                    }

                    //If the connection timeout is not set for this alias then 
                    //set it to 10 seconds
                    if (tConnectTimeout <= 0) {
                        tConnectTimeout = 10;
                        System.out.println("DBMojoServer Warning, 'connectTimeout' not " + "set for alias '"
                                + tAlias + "' using 10 seconds");
                    }

                    //Make sure another alias with the same name is not already 
                    //defined in the config
                    if (dbPools.containsKey(tAlias)) {
                        throw new Exception(
                                "the alias '" + tAlias + "' is already defined in " + " the provided config file");
                    }

                    //Everything is nicely set! Lets add a connection pool to the 
                    //dbPool Hashtable keyed by this alias name
                    dbPools.put(tAlias, new JDBCConnectionPool(tDriver, tDsn, tUsername, tPassword, tMaxConnections,
                            tExpirationTime, tConnectTimeout, tAlias));
                }
            }

            loadClusters: {
                final JSONArray tClusters = jObj.optJSONArray("clusters");

                if (tClusters != null) {
                    for (int c = 0; c < tClusters.length(); c++) {
                        final JSONObject tObj = tClusters.getJSONObject(c);
                        final String tAlias = tObj.getString("alias");
                        final String tWriteTo = tObj.getString("writeTo");

                        if (dbPools.containsKey(tAlias)) {
                            throw new Exception("the alias '" + tAlias + "' is already defined.");
                        }

                        if (!dbPools.containsKey(tWriteTo)) {
                            throw new Exception(
                                    "the alias '" + tWriteTo + "' is not present in the valid dbAliases. "
                                            + "This alias cannot be used for a cluster.");
                        }

                        //Add the dbAlias to the cluster writeTo list
                        ConnectionPool writeTo = dbPools.get(tWriteTo);

                        final JSONArray tReadFrom = tObj.getJSONArray("readFrom");
                        ArrayList<ConnectionPool> readFromList = new ArrayList<ConnectionPool>();
                        for (int r = 0; r < tReadFrom.length(); r++) {
                            final String tRead = tReadFrom.getString(r);
                            if (!dbPools.containsKey(tRead)) {
                                throw new Exception(
                                        "the alias '" + tRead + "' is not present in the valid dbAliases. "
                                                + "This alias cannot be used for a cluster.");
                            }
                            //Add the dbAlias to the cluster readFrom list
                            readFromList.add(dbPools.get(tRead));
                        }

                        dbPools.put(tAlias, new JDBCClusteredConnectionPool(tAlias, writeTo, readFromList));
                    }
                }
            }

            server = new DBMojoServer(useGzip, serverPort, maxConcReq, dbPools);

        } catch (Exception jsonEx) {
            System.out.println("DBMojoServer: Config error, " + jsonEx);
            System.exit(-1);
        }

        return server;
    }

    /** Read the config file and spawn the DBMojo server instance. */
    public static void main(String[] args) {

        //Create DBMojoServer object from config file
        //This also starts all the necessary loggers
        DBMojoServer mojo = getMojoServerFromConfig(args);

        //Attempt to start the server
        try {
            mojo.start();
        } catch (Exception e) {
            if (ErrorLog.enabled) {
                ErrorLog.add(mojo, "Service could not be started: " + e, true);
            }
            System.exit(-1);
        }

        //Welcome message
        System.out.println(
                "\nDBMojoServer: Now listening on port: " + mojo.serverPort + "\nPress 'q + Enter' to exit\n");

        //Wait until the user enters: q + Enter to stop the server
        //113 is the ASCII code for q
        try {
            while (true) {
                if (System.in.read() == 113) {
                    mojo.stop();
                    if (DebugLog.enabled) {
                        DebugLog.add(mojo, "Service has exitted normally");
                    }
                    System.exit(0);
                }
            }
        } catch (Exception se) {
            if (ErrorLog.enabled) {
                ErrorLog.add(mojo, "Service has exited abnormally", true);
            }
        }
    }
}