/*
* (C) Copyright IBM Corp. 2012
*
* LICENSE: Eclipse Public License v1.0
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.ibm.gaiandb.webserver;
/**
* @author Patrick Dantressangle
* a prototype of a very simple, multi-threaded HTTP server for Serving GaianDB pre-canned SQL queries
* This is reusing code as-is from the WebQueryModule, with some modifications as it is not part of a proper servlet server.
* for instance, it is using a StringBuffer to return the page instead of using a stream.
* But for now, this is good enough to demonstrated how this function can work minimally ... in 36K jar!! ;-)
*
**/
import java.io.*;
import java.net.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;
import com.ibm.gaiandb.GaianDBConfig;
import com.ibm.gaiandb.GaianNode;
import java.sql.Clob;
import java.sql.Blob;
//TODO: these 3 import are not used for now. They will probably be to consolidate webquerymodule code and wwwserver code.
//import javax.servlet.ServletException;
//import javax.servlet.http.HttpServletRequest;
//import javax.servlet.http.HttpServletResponse;
//types of queries:
// http://localhost:8080/WebQueryModule/resources/queries/LT0byNode/excel?GDB_NODE=Patrick_W150&PARAM2=Value2
// http://localhost:8080/WebQueryModule/resources/queries/LT0byNode?GDB_NODE=Patrick_W150&PARAM2=Value2
// http://localhost:8080/JavaSource/com/ibm/gaiandb/EntityMatrixJoiner.java
// http://localhost:8080/
public class WebServer {
//GaianDB/Derby specific information
protected static final String ndriver = GaianDBConfig.DERBY_CLIENT_DRIVER;
protected static final String edriver = GaianDBConfig.DERBY_EMBEDDED_DRIVER;
protected static final boolean standalone = false;
protected static final String DEFAULT_USR = GaianDBConfig.GAIAN_NODE_DEFAULT_USR;
protected static final String DEFAULT_PWD = GaianDBConfig.GAIAN_NODE_DEFAULT_PWD;
protected static final String DEFAULT_HOST = "localhost";
protected static final int DEFAULT_PORT = GaianNode.DEFAULT_PORT;
protected static final String DEFAULT_DATABASE = GaianDBConfig.GAIANDB_NAME;
protected static final String DBMS = "derby";
//TODO:WQM specific information ;may need to go away when refactoring of WQM is done
protected static final String WQM_QueryPrefix="WebQueryModule/resources/queries/";
protected static final String QUERY_TABLE = "GDB_Local_Queries";
protected static final String QUERY_FIELDS_TABLE = "GDB_Local_Query_Fields";
protected static final String RESPONSE_FORMAT_BLOB = "blob";
protected static final String RESPONSE_FORMAT_BLOB_GZIPPED = "blobgzipped";
protected static final String RESPONSE_FORMAT_CLOB = "clob";
protected static enum ResponseFormat { JSON, EXCEL, BLOB, BLOB_GZIPPED, CLOB };
/* 2XX: OK codes */
public static final int HTTP_OK = 200;
public static final int HTTP_CREATED = 201;
public static final int HTTP_ACCEPTED = 202;
public static final int HTTP_NOT_AUTHORITATIVE = 203;
public static final int HTTP_NO_CONTENT = 204;
public static final int HTTP_RESET = 205;
public static final int HTTP_PARTIAL = 206;
/* 3XX: relocation or redirect */
public static final int HTTP_MULT_CHOICE = 300;
public static final int HTTP_MOVED_PERM = 301;
public static final int HTTP_MOVED_TEMP = 302;
public static final int HTTP_SEE_OTHER = 303;
public static final int HTTP_NOT_MODIFIED = 304;
public static final int HTTP_USE_PROXY = 305;
/* 4XX: client error */
public static final int HTTP_BAD_REQUEST = 400;
public static final int HTTP_UNAUTHORIZED = 401;
public static final int HTTP_PAYMENT_REQUIRED = 402;
public static final int HTTP_FORBIDDEN = 403;
public static final int HTTP_NOT_FOUND = 404;
public static final int HTTP_BAD_METHOD = 405;
public static final int HTTP_NOT_ACCEPTABLE = 406;
public static final int HTTP_PROXY_AUTH = 407;
public static final int HTTP_CLIENT_TIMEOUT = 408;
public static final int HTTP_CONFLICT = 409;
public static final int HTTP_GONE = 410;
public static final int HTTP_LENGTH_REQUIRED = 411;
public static final int HTTP_PRECON_FAILED = 412;
public static final int HTTP_ENTITY_TOO_LARGE = 413;
public static final int HTTP_REQ_TOO_LONG = 414;
public static final int HTTP_UNSUPPORTED_TYPE = 415;
/* 5XX: server error */
public static final int HTTP_SERVER_ERROR = 500;
public static final int HTTP_INTERNAL_ERROR = 501;
public static final int HTTP_BAD_GATEWAY = 502;
public static final int HTTP_UNAVAILABLE = 503;
public static final int HTTP_GATEWAY_TIMEOUT = 504;
public static final int HTTP_VERSION = 505;
/* static class data/methods */
static boolean debug = false;
/* console print */
protected static void console_print(String s) {
if (debug)
System.out.println(s);
}
/* print to the log file */
protected static void log(String s) {
if (debug){
synchronized (log) {
log.println(s);
log.flush();
}
}
}
static PrintStream log = null;
/* our server's configuration information is stored
* in these properties
*/
protected static Properties ConfigurationProperties = new Properties();
/* Where worker threads stand idle */
static Vector<PageWorker> threads = new Vector<PageWorker>();
/* the web server's virtual root */
static File root;
static boolean directory_browsing;
/* timeout on client connections */
static int timeout = 0;
/* max # worker threads */
static int maxpageWorkers = 5;
/* used when shutting down the server*/
protected boolean running =false;
/* default port */
private static int port = 8080;
// used for HTML end of lines
static final String EOL ="\r\n";
//{(byte)'\r', (byte)'\n' };
/***************************************************************
* load www-server.properties from java.home
* just basic setting for now.
**************************************************************/
static void loadConfiguration() throws IOException {
//File f = new File(System.getProperty("java.home")+File.separator+"lib"+File.separator+"GDBWWW.properties");
File f=new File(System.getProperty("user.dir")+ File.separator+"GDBWWW.properties");
//System.out.println("Taking default values as GDB WWW configuration file does not exist: ["+f.getAbsolutePath()+"]");
if (f.exists()) {
InputStream is =new BufferedInputStream(new FileInputStream(f));
ConfigurationProperties.load(is);
is.close();
String r = ConfigurationProperties.getProperty("root");
if (r != null) {
root = new File(r);
if (!root.exists()) {
//throw new Error
System.out.println(root + " doesn't exist as server root. Taking defaults for root");
root=null;
}
}
r = ConfigurationProperties.getProperty("directory_browsing");
if (r != null){
directory_browsing=(r.trim().equalsIgnoreCase("true"));
}
r = ConfigurationProperties.getProperty("debug");
if (r != null){
debug=(r.trim().equalsIgnoreCase("true"));
}
r = ConfigurationProperties.getProperty("timeout");
if (r != null) {
timeout = Integer.parseInt(r.trim());
}
r = ConfigurationProperties.getProperty("port");
if (r != null) {
port = Integer.parseInt(r.trim());
}
r = ConfigurationProperties.getProperty("workers");
if (r != null) {
maxpageWorkers = Integer.parseInt(r);
}
r = ConfigurationProperties.getProperty("log");
if (r != null) {
r=r.replaceAll("//", File.separator);
console_print("opening log file: " + r);
try{
log = new PrintStream(new BufferedOutputStream( new FileOutputStream(r)));
}
catch (FileNotFoundException e){
System.out.println("The system cannot find the path specified["+r+"].revert to default behavior");
log=null;
}
}
}
else
System.out.println("Taking default values as GDB WWW configuration file does not exist: ["+f.getAbsolutePath()+"]");
/* if no properties were specified, choose defaults */
if (root == null) {
root = new File(System.getProperty("user.dir"));
}
if (timeout <= 1000) {
timeout = 5000;
}
/* put a minimum amount of workers..*/
if (maxpageWorkers < 5) {
maxpageWorkers = 5;
}
if (log == null) {
console_print("logging to stdout");
log = System.out;
}
}
/**
* just basic dump of configuration if required.
*/
static void printconfiguration() {
console_print("root="+root);
console_print("timeout="+timeout);
console_print("workers="+maxpageWorkers);
}
/**
* used to kick start it from the command line
* @param a
* @throws Exception
*/
public static void main(String[] a) throws Exception {
if (a.length > 0) {
port = Integer.parseInt(a[0]);
}
loadConfiguration();
printconfiguration();
/* start worker threads */
for (int i = 0; i < maxpageWorkers; ++i) {
PageWorker w = new PageWorker();
(new Thread(w, "worker #"+i)).start();
threads.addElement(w);
}
ServerSocket ss = new ServerSocket(port);
System.out.println("GDBWWW server started on port "+port);
while (true) {
Socket s = ss.accept();
PageWorker w = null;
synchronized (threads) {
if (threads.isEmpty()) {
PageWorker ws = new PageWorker();
ws.setSocket(s);
(new Thread(ws, "additional worker")).start();
} else {
w = threads.elementAt(0);
threads.removeElementAt(0);
w.setSocket(s);
}
}
}
}
}
/*********************************************************
* This worker is in charge of serving the page and all GDB queries
* @author dantress
*
**********************************************************/
class PageWorker extends WebServer implements Runnable {
final static int BUFFER_SIZE = 2048;
/* buffer to use for requests */
byte[] buffer;
/* Socket to client we're handling */
private Socket remoteSocket=null;
/* have one SQL connection per worker and reuse it */
static Connection conn=null;
PageWorker() {
buffer = new byte[BUFFER_SIZE];
}
PageWorker(Socket rs) throws Exception {
buffer = new byte[BUFFER_SIZE];
if (rs !=null)
remoteSocket = rs;
}
synchronized void setSocket(Socket s) {
this.remoteSocket = s;
notify();
}
public synchronized void run() {
while(true) {
if (remoteSocket == null) {
try {
wait();
} catch (InterruptedException e) {
/* should not happen */
continue;
}
}
try {
handleClient();
} catch (Exception e) {
e.printStackTrace();
}
/* go back in wait queue if there's fewer than numHandler connections.*/
remoteSocket = null;
Vector<PageWorker> threadPool = WebServer.threads;
synchronized (threadPool) {
if (threadPool.size() >= WebServer.maxpageWorkers) {
/* too many threads, exit this one */
return;
} else {
threadPool.addElement(this);
}
}//endofsynchronized code
}//endofwhile
}
/*********************************************************
* Handle HTML client request: Mainly GET and HEAD for now. POST for later.
* @throws IOException
*********************************************************/
void handleClient() throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader( remoteSocket.getInputStream()));
PrintStream ps = new PrintStream(remoteSocket.getOutputStream());
/* we will only block in read for this many milliseconds
* before we fail with java.io.InterruptedIOException,
* at which point we will abandon the connection.
*/
remoteSocket.setSoTimeout(WebServer.timeout);
remoteSocket.setTcpNoDelay(true);
try {
// Read data from client - we only really care about the first line, but read it all
String currentLine = in.readLine();
String headerLine = currentLine;
if (headerLine==null)
return;
StringTokenizer tokenizer = new StringTokenizer(headerLine);
String HttpMethod=tokenizer.nextToken();
/* is this request a GET/POST or just a HEAD? */
boolean doingGet;
if ( HttpMethod.equals("GET")){
doingGet = true;
}
else if ( HttpMethod.equals("HEAD")){
doingGet = false;
} else {
/* we don't support this method */
ps.println("HTTP/1.0 " + HTTP_BAD_METHOD +" unsupported method type: "+HttpMethod);
ps.flush();
remoteSocket.close();
return;
}
/* find the file name, from: GET /xxx/mypage.html HTTP/1.0
* extract "/xxx/mypage.html"
*/
String fname = tokenizer.nextToken();
if (fname.startsWith("/")) {
fname = fname.substring(1);
}
//urls of style : http://localhost:8080/WebQueryModule/...
if (doingGet)
{
processHTTPGET(ps, doingGet,fname);
}
} finally {
remoteSocket.close();
}
}
/**********************************************************************
* Process a GDB specific GET METHOD (mainly queries and other pre-canned things )
* @param ps
* @param doingGet
* @param fname
* @throws IOException
*********************************************************************/
private void processHTTPGET(PrintStream ps, boolean doingGet,String fname) throws IOException {
if(fname.toLowerCase().contains("dashboard/"))
{
processDashboardQuery(ps, doingGet,fname);
}
else
if(fname.startsWith("WebQueryModule/"))
{ //process specific GaianDB queries
processGDBQuery(ps, doingGet,fname);
}
else { //process basic files/pages...
File targ = new File(WebServer.root, fname);
if (targ.isDirectory()) {
File ind = new File(targ, "index.html");
if (ind.exists()) {
targ = ind;
}
}
boolean OK = printHeaders(null,targ, ps);
if (doingGet) {
if (OK) {
sendFile(targ, ps);
} else {
sendHTTPError(404,targ.getAbsolutePath(), ps);
}
}
}
}
/************************************************
* process pre-canned dashboard queries..instead of using the JAVA application.
* Here is the mapping of URLS..
* LISTQUERIES: /Dashboard/ListLocalQueries
*
* @param ps
* @param doingGet
* @param fname
***********************************************/
private void processDashboardQuery(PrintStream out, boolean doingGet, String request)
{
try{
if (conn ==null)
conn = getDbConnection(DEFAULT_DATABASE,DEFAULT_HOST,DEFAULT_PORT,DEFAULT_USR, DEFAULT_PWD);
Statement statement = conn.createStatement();
try {
if(request.toLowerCase().endsWith("dashboard/listqueries"))
executeQueryFromQueryString("CALL LISTQUERIES(0)", null, ResponseFormat.EXCEL);
else {
System.out.println("argh..."+request);
sendHTTPError(404,request, out);
}
} finally {
statement.close();
}
}
//humm... couldn't create the query...send 404 error...
catch (SQLException e) {
System.err.println("GaianDB WebServer: Unable to access a running GaianDB ");
e.printStackTrace();
sendHTTPError(500,request, out);
}
catch (Exception e) {
System.err.println("GaianDB WebServer: Unable to process request/query ");
e.printStackTrace();
sendHTTPError(500,request, out);
}
}
/***************************************************
* Load the derby driver.
* @param driver
*************************************************/
private static boolean driverloaded=false;
protected void loadDriver( String driver ) {
try {
if (driverloaded==false)
{
Class.forName( driver ).newInstance();
driverloaded=true;
}
} catch (Exception e) {
e.printStackTrace();
}
}
/*************************************************
* Get a GaianDB /Derby connection. (taken from Derbyrunner )
*************************************************/
private Connection getDbConnection(String mDatabase, String mHost, int mPort, String mUser, String mPasswd) throws SQLException {
if ((conn ==null) || (conn.isClosed()))
{
String url;
if ( standalone ) {
loadDriver( edriver );
url = "jdbc:" + DBMS + ":" + mDatabase + ";"; //create=" + createdb + (upgrade?";upgrade=true":"");
} else {
loadDriver( ndriver );
url = "jdbc:" + DBMS + "://" + mHost + ":" + mPort + "/" + mDatabase + ";"; //create=" + createdb + (upgrade?";upgrade=true":"");
}
conn= DriverManager.getConnection( url, mUser, mPasswd );
}
return conn;
}
/**********************************************************
* TODO:Taken directly from WQM : list queries.... need to be refactored out
*********************************************************/
public void listQueries(StringBuffer out, ResponseFormat RetFormat) throws SQLException, IOException {
try {
Statement statement = conn.createStatement();
try {
ResultSet results = statement.executeQuery("SELECT id, description,issuer,last_extracted,query FROM " + QUERY_TABLE);
try {
if (RetFormat == ResponseFormat.JSON )
out.append('[');
else out.append("<HTML><TITLE>List of queries</TITLE><BODY><H1>List of queries</H1><TABLE BORDER=1><TR><TH>Id<TH>Description<TH>Issuer<TH>Last Extracted</TR>");
boolean first = true;
while (results.next()) {
if (first) {
first = false;
} else {
if (RetFormat == ResponseFormat.JSON )
out.append(",\n");
//else out.append("</TR><TR>");
}
if (RetFormat == ResponseFormat.JSON )
{
out.append("{\"id\":\"" + results.getString("id") + "\",");
out.append("\"description\":\"" + results.getString("description") + "\"}");
out.append("\"issuer\":\"" + results.getString("issuer") + "\"}");
out.append("\"last_extracted\":\"" + results.getString("last_extracted") + "\"}");
}
else
{
String Qid=results.getString("id");
out.append("<TR><TD><A HREF=\"/"+WQM_QueryPrefix+Qid+"/excel\">"+Qid+"</A></TD>"+
"<TD>"+results.getString("description")+"</TD>"+
"<TD>"+results.getString("issuer")+"</TD>"+
"<TD>"+results.getString("last_extracted")+"</TD>"+
"</TR>"+EOL);
}
}
if (RetFormat == ResponseFormat.JSON )
out.append(']');
else out.append("</TABLE></BODY></HTML>");
} finally {
results.close();
}
} finally {
statement.close();
}
} finally {
//don't close in WWGDBserver so it can be reused...
//TODO:WQM: conn.close();
}
}
/********************************************************8
* Get the Query parameters....
* @param query
* @return
******************************************************/
public static Map<String, String> getQueryParameters(String query)
{
Map<String, String> map = new HashMap<String, String>();
if (query !=null){
String[] params = query.split("&");
for (String param : params)
{
String name = param.split("=")[0];
String value = param.split("=")[1];
map.put(name, value);
}
}
return map;
}
// protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// request.
// }
/***********************************************************************
* Process GDB WQM queries... try to mimic WQM Servlet module here..obviously incomplete.
* @param ps
* @param request
*********************************************************************/
private void processGDBQuery(PrintStream ps, boolean doingGet,String request) {
String[] parmsStr = request.split("\\?");
String[] parms = parmsStr[0].split("/");
// @SuppressWarnings("unchecked")
Map<String, String> queryParams =null;
if (parmsStr.length>1)
queryParams = getQueryParameters(parmsStr[1]); //in WQM: .request.getParameterMap();
String queryId =parms[3];
ResponseFormat responseFormat = ResponseFormat.JSON;
//extract query name.
try{
if ((conn ==null)|| (conn.isClosed()))
conn = getDbConnection(DEFAULT_DATABASE,DEFAULT_HOST,DEFAULT_PORT,DEFAULT_USR, DEFAULT_PWD);
StringBuffer Query_output=new StringBuffer("");//<HTML><BODY><TITLE>Found tokens</TITLE><H1>Found the following tokens</H1><PRE>");
if (parms.length>=5){
if (parms[4].equalsIgnoreCase("excel"))
responseFormat=ResponseFormat.EXCEL;
}
if (request.startsWith(WQM_QueryPrefix+"ListQueries"))
{
listQueries(Query_output,responseFormat);
}
else {
Query_output.append( executeQueryFromQueryID( queryId, queryParams, responseFormat));
//for debugging purposes only...
// Query_output.append("request url:"+request+EOL);
// for (String p: parms) {
// Query_output.append("Found token:"+p+EOL);
// };
// Query_output.append("Not recognized query or command"+EOL);
}
//Query_output.append("</PRE></BODY></HTML>");
boolean OK = printHeaders(Query_output,null, ps);
if (doingGet){
if (OK) {
if (debug)
console_print(Query_output.toString());
ps.println(Query_output);
ps.flush();
} else {
sendHTTPError(404,request, ps);
}
}
}
//humm... couldn't create the query...send 404 error...
catch (SQLException e) {
if (debug){
System.err.println("GaianDB WebServer: Unable to access a running GaianDB ");
e.printStackTrace();
}
sendHTTPError(500,request, ps);
}
catch (Exception e) {
if (debug){
System.err.println("GaianDB WebServer: Unable to process request/query ");
e.printStackTrace();
}
sendHTTPError(500,request, ps);
}
}
/***************************************************************************************
*TODO: WQM: should be factored out when WQM code is refactored independant of Servlet,
*
*************************************************************************************/
private String getRequestFieldValue(String field, Map<String, String> queryParams) throws Exception {
String fieldValues = queryParams.get(field); //WQM removed []
if (fieldValues == null) {
throw new Exception("Missing parameter to query: \"" + field + "\".");
}
//WQM: else if (fieldValues.length != 1) {
//WQM: throw new Exception("Multiple values provided for parameter \"" + field + "\".");
//WQM:}
return fieldValues; //WQM[0];
}
/*****************************************************************************************8
* this method is for executing Queries directly (not using the table of queries)
* mainly to have calls to SQL procedures directly mapped to URLs
* @param queryTemplate
* @param queryParams
* @param responseFormat
* @return
* @throws Exception
**************************************************************************************/
public StringBuffer executeQueryFromQueryString( String queryTemplate, Map<String, String> queryParams, ResponseFormat responseFormat) throws Exception {
log("Executing query '" + queryTemplate + "'");
Connection conn = getDbConnection(DEFAULT_DATABASE,DEFAULT_HOST,DEFAULT_PORT,DEFAULT_USR, DEFAULT_PWD);
StringBuffer response=new StringBuffer("");
//ResponseFormat responseFormatString=ResponseFormat.EXCEL;
PreparedStatement statement= conn.prepareStatement(queryTemplate);
try {
//not used for now: ParameterMetaData pmd = statement.getParameterMetaData();
//not useful for now... : int numParams = pmd.getParameterCount();
boolean returnedResultSet = statement.execute();
ResultWriter rw = new HtmlResultWriter(response);
if (returnedResultSet) {
ResultSet results = statement.getResultSet();
try {
ResultSetMetaData rsmd = results.getMetaData();
int colCount = rsmd.getColumnCount();
String[] cols = new String[colCount];
for (int i = 0; i < colCount; i++) {
cols[i] = rsmd.getColumnLabel(i + 1);
}
rw.writeHeader(cols);
boolean first = true;
while (results.next()) {
if (first) {
first = false;
} else {
rw.writeRowSeparator();
}
Object[] values = new Object[colCount];
for (int i = 0; i < colCount; i++) {
values[i] = results.getObject(i + 1);
}
rw.writeRow(values);
}
rw.writeFooter();
} finally {
results.close();
}
} else {
rw.writeUpdateCount(statement.getUpdateCount());
}
} finally {
statement.close();
}
return response;
}
/***************************************************************************************
* TODO:WQM: should be factored out when WQM code is refactored independent of Servlet,
*
*************************************************************************************/
public StringBuffer executeQueryFromQueryID( String queryId, Map<String, String> queryParams, ResponseFormat responseFormat) throws Exception {
log("Executing query '" + queryId + "'");
Connection conn = getDbConnection(DEFAULT_DATABASE,DEFAULT_HOST,DEFAULT_PORT,DEFAULT_USR, DEFAULT_PWD);
StringBuffer response=new StringBuffer("");
try {
// Get query template from database
String queryTemplate=null, responseFormatString=null;
PreparedStatement statement = conn.prepareStatement("SELECT query, response_format FROM " + QUERY_TABLE + " WHERE id = ?");
try {
statement.setString(1, queryId);
ResultSet results = statement.executeQuery();
try {
if (results.next()) {
queryTemplate = results.getString("query");
responseFormatString = results.getString("response_format");
}
// TODO: Should be processed above in GDBWW server , sending back either a 404 or 500 HTTP error.
else {
throw new SQLException("Could not find query \"" + queryId + "\"");
}
} finally {
results.close();
}
} finally {
statement.close();
}
// Both JSON and EXCEL response formats will be overridden by BLOB/CLOB
if (responseFormatString != null) {
if (responseFormatString.equals(RESPONSE_FORMAT_BLOB)) {
responseFormat = ResponseFormat.BLOB;
} else if (responseFormatString.equals(RESPONSE_FORMAT_BLOB_GZIPPED)) {
responseFormat = ResponseFormat.BLOB_GZIPPED;
} else if (responseFormatString.equals(RESPONSE_FORMAT_CLOB)) {
responseFormat = ResponseFormat.CLOB;
}
}
// Get fields from database
List<String> fields = new ArrayList<String>();
SortedMap<Short, String> substitutionFields = new TreeMap<Short, String>();
statement = conn.prepareStatement("SELECT name, seq, offset FROM " + QUERY_FIELDS_TABLE + " WHERE query_id = ? ORDER BY seq, offset");
try {
statement.setString(1, queryId);
ResultSet results = statement.executeQuery();
try {
while (results.next()) {
if (results.getObject("seq") != null) {
fields.add(results.getString("name"));
} else {
substitutionFields.put(results.getShort("offset"), results.getString("name"));
}
}
} finally {
results.close();
}
} finally {
statement.close();
}
// Execute query with passed parameters
// Do query variable substitution
int fieldLengthDelta = 0;
if (queryParams != null) {
for ( Map.Entry<Short, String> f : substitutionFields.entrySet() ) {
String fieldValue = getRequestFieldValue(f.getValue(), queryParams);
int actualOffset = f.getKey() + fieldLengthDelta;
//String sanitisedValue = sanitiseField(fieldValue, queryTemplate, actualOffset);
String sanitisedValue = fieldValue;
queryTemplate = queryTemplate.substring(0, actualOffset) + sanitisedValue + queryTemplate.substring(actualOffset);
fieldLengthDelta += sanitisedValue.length();
}
}
log("Preparing query template with substituted params: " + queryTemplate);
statement = conn.prepareStatement(queryTemplate);
try {
ParameterMetaData pmd = statement.getParameterMetaData();
int numParams = pmd.getParameterCount();
if(fields.size() != numParams) {
throw new Exception("Invalid query: number of field names does not match number of parameters");
}
if(queryParams != null)
{
for (int i = 0; i < fields.size(); i++) {
statement.setObject(i + 1, getRequestFieldValue(fields.get(i), queryParams));
}
}
boolean returnedResultSet = statement.execute();
ResultWriter rw = null;
switch (responseFormat) {
case EXCEL:
rw = new HtmlResultWriter(response); //TODO: WQM.getWriter());
break;
case JSON:
rw = new JsonResultWriter(response); //TODO:.getWriter());
break;
case BLOB:
rw = new BlobResultWriter(response);
break;
/*** WQM: not supporting this for now in GDB WWW Server
case BLOB_GZIPPED:
rw = new GzippedBlobResultWriter(request, response);
break;
case CLOB:
rw = new ClobResultWriter(response);
break;
***/
}
if (returnedResultSet) {
ResultSet results = statement.getResultSet();
try {
ResultSetMetaData rsmd = results.getMetaData();
int colCount = rsmd.getColumnCount();
String[] cols = new String[colCount];
for (int i = 0; i < colCount; i++) {
cols[i] = rsmd.getColumnLabel(i + 1);
}
rw.writeHeader(cols);
boolean first = true;
while (results.next()) {
if (first) {
first = false;
} else {
rw.writeRowSeparator();
}
Object[] values = new Object[colCount];
for (int i = 0; i < colCount; i++) {
values[i] = results.getObject(i + 1);
}
rw.writeRow(values);
}
rw.writeFooter();
} finally {
results.close();
}
} else {
rw.writeUpdateCount(statement.getUpdateCount());
}
} finally {
statement.close();
}
} finally {
//conn.close();
}
return response;
}
/*************************************************************
* Print the proper header from scratch. doesn't try to mimic servlet headers
*
***************************************************************/
boolean printHeaders(StringBuffer queryOutput, File targ, PrintStream ps) throws IOException {
boolean ret = false;
int rCode = 0;
String hostaddress=remoteSocket.getInetAddress().getHostAddress();
if (null != targ && !targ.exists()) {
rCode = HTTP_NOT_FOUND;
ps.println("HTTP/1.1 " + HTTP_NOT_FOUND + " not found");
ret = false;
log("Page request from " +hostaddress+": GET " + targ.getAbsolutePath()+"-->"+rCode);
} else {
rCode = HTTP_OK;
ps.println("HTTP/1.1 " + HTTP_OK+" OK");
ret = true;
if (targ ==null && queryOutput != null)
log("Page request from " +hostaddress+": GET "+queryOutput+"-->"+rCode);
else
log("Page request from " +hostaddress+": GET " +targ.getAbsolutePath()+"-->"+rCode);
}
ps.println("Server: GaianDB WWW server");
ps.println("Date: " + (new Date()));
if ((ret) ) {
if (queryOutput != null)
{
int ql=queryOutput.length()+1;
ps.println("Content-Length: "+ql);
ps.println("Last Modified: " + (new Date()));
ps.println("Cache-Control: no-cache");
ps.println("Pragma: no-cache");
ps.println("Content-Type: text/html");
}
else
if ( (null != targ) && !targ.isDirectory()) {
ps.println("Content-Length: "+targ.length());
ps.println("Last Modified: " + (new Date(targ.lastModified())));
String name = targ.getName();
int ind = name.lastIndexOf('.');
String ct = null;
if (ind > 0) {
ct = map.get(name.substring(ind));
}
if (ct == null) {
ct = "unknown/unknown";
}
ps.println("Content-Type: " + ct);
} else {
ps.println("Content-Type: text/html");
}
// Blank line indicates end of headers
ps.println();
}
return ret;
}
/*****************************************************************
* return a HTTP error from trying to fetch a file back
* @param targ
* @param ps
* @throws IOException
*****************************************************************/
void sendHTTPError(int HTTPerror, String reason, PrintStream ps) {
try {
ps.println();
ps.println();
switch (HTTPerror){
case 400: ps.println("The request ("+reason+") cannot be fulfilled due to bad syntax"); break;
case 404: ps.println("The requested resource ("+reason+") could not be found but may be available again in the future."); break;
case 405: ps.println("A request was made of a page using a request method not supported by that page"); break;
case 500: ps.println("Server error\n"+ "The Server failed to process the query:"+reason+"."); break;
default : ps.println("An undertermined error ("+HTTPerror+") occured with reason:"+reason);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/*****************************************************************
* Send a file back to client.
* @param targ
* @param ps
* @throws IOException
*****************************************************************/
void sendFile(File targ, PrintStream out) throws IOException {
InputStream in = null;
out.println();
if (directory_browsing ==false)
return;
if (targ.isDirectory()) {
listDirectory(targ, out);
return;
} else {
in = new BufferedInputStream(new FileInputStream(targ.getAbsolutePath()));
}
try {
byte[] buffer = new byte[BUFFER_SIZE];
int byteCount;
while ( (byteCount = in.read(buffer)) != -1 ) {
out.write(buffer, 0, byteCount);
out.flush();
}
} finally {
in.close();
}
}
/*****************************************************************
* mapping of file extensions to content-types
* @param targ
* @param ps
* @throws IOException
*****************************************************************/
static java.util.Hashtable<String, String> map = new java.util.Hashtable<String, String>();
static {
fillMap();
}
static void setHttpApplicationType(String k, String v) {
map.put(k, v);
}
static void fillMap() {
setHttpApplicationType("", "content/unknown");
setHttpApplicationType(".uu", "application/octet-stream");
setHttpApplicationType(".exe", "application/octet-stream");
setHttpApplicationType(".ps", "application/postscript");
setHttpApplicationType(".zip", "application/zip");
setHttpApplicationType(".sh", "application/x-shar");
setHttpApplicationType(".tar", "application/x-tar");
setHttpApplicationType(".snd", "audio/basic");
setHttpApplicationType(".au", "audio/basic");
setHttpApplicationType(".wav", "audio/x-wav");
setHttpApplicationType(".gif", "image/gif");
setHttpApplicationType(".jpg", "image/jpeg");
setHttpApplicationType(".jpeg", "image/jpeg");
setHttpApplicationType(".htm", "text/html");
setHttpApplicationType(".html", "text/html");
setHttpApplicationType(".text", "text/plain");
setHttpApplicationType(".c", "text/plain");
setHttpApplicationType(".cc", "text/plain");
setHttpApplicationType(".c++", "text/plain");
setHttpApplicationType(".h", "text/plain");
setHttpApplicationType(".pl", "text/plain");
setHttpApplicationType(".txt", "text/plain");
setHttpApplicationType(".java", "text/plain");
}
/***********************************************************************
* List a directory in HTML with HREF for files
* @param dir
* @param ps
* @throws IOException
*********************************************************************/
void listDirectory(File dir, PrintStream ps) throws IOException {
ps.println("<TITLE>Directory listing</TITLE><P>\n");
ps.println("<A HREF=\"..\">Parent Directory</A><BR>\n");
String[] list = dir.list();
for (int i = 0; list != null && i < list.length; i++) {
File f = new File(dir, list[i]);
if (f.isDirectory()) {
ps.println("<A HREF=\""+list[i]+"/\">"+list[i]+"/</A><BR>");
} else {
ps.println("<A HREF=\""+list[i]+"\">"+list[i]+"</A><BR>");
}
}
ps.println("<P><HR><BR><I>" + (new Date()) + "</I>");
}
/**********************************************************************
* shutdown the server on user command..
*********************************************************************/
public void shutdown() {
System.err.println("GaianDB WebServer: Attemping to shutdown...");
running = false;
try {
remoteSocket.close();
} catch (IOException e) {
System.err.println("GaianDB WebServer: Unable to close server socket (?!)");
e.printStackTrace();
}
}
}
/****************************************************************************
* TODO: Taken from WQM... would need to be factored out when WQM is rewritten more generically.
* @author Dominic Harries
***************************************************************************/
interface ResultWriter {
// Use PROPRIETARY notice if class contains a main() method, otherwise use COPYRIGHT notice.
public static final String COPYRIGHT_NOTICE = "(c) Copyright IBM Corp. 2011";
public void writeHeader(String[] cols) throws IOException, Exception;
public void writeFooter() throws IOException;
public void writeRow(Object[] values) throws IOException, Exception;
public void writeRowSeparator() throws IOException, Exception;
public void writeUpdateCount(int updateCount) throws IOException, Exception;
}
/****************************************************************************
* TODO:Taken from WQM... would need to be factored out when WQM is rewritten more generically.
* @author Dominic Harries
**************************************************************************/
class HtmlResultWriter implements ResultWriter {
// Use PROPRIETARY notice if class contains a main() method, otherwise use COPYRIGHT notice.
public static final String COPYRIGHT_NOTICE = "(c) Copyright IBM Corp. 2011";
private StringBuffer out;
public HtmlResultWriter(StringBuffer out) {
this.out = out;
}
public void writeHeader(String[] cols) throws IOException {
out.append("<html><head><title>Query Results</title></head><body><table border=\"1\"><tr>");
for (String col: cols) {
out.append("<th>");
out.append(col);
out.append("</th>");
}
out.append("</tr>");
}
public void writeFooter() throws IOException {
out.append("</table></body>");
}
public void writeRow(Object[] values) throws IOException {
out.append("<tr>");
for (Object value : values) {
out.append("<td>" + value + "</td>");
}
out.append("</tr>");
}
public void writeRowSeparator() {
// Not required
}
public void writeUpdateCount(int updateCount) throws IOException {
out.append(String.valueOf(updateCount));
}
}
/*****************************************************************************
* TODO:Taken from WQM... would need to be factored out when WQM is rewritten more generically.
* @author Dominic Harries
****************************************************************************/
class JsonResultWriter implements ResultWriter {
// Use PROPRIETARY notice if class contains a main() method, otherwise use COPYRIGHT notice.
public static final String COPYRIGHT_NOTICE = "(c) Copyright IBM Corp. 2011";
StringBuffer out; //WQM TODO: private Writer out;
private String[] cols;
public JsonResultWriter(StringBuffer response) {
this.out = response;
}
public void writeHeader(String[] cols) throws IOException {
this.cols = cols;
out.append("{\"columns\": ["); //TODO:
for (int i = 0; i < cols.length; i++) {
out.append("\"" + cols[i] + "\""); //TODO:
if (i < cols.length - 1) out.append(','); //TODO:
}
out.append("],\n"); //TODO:
out.append("\"items\": [\n"); //TODO:
}
public void writeFooter() throws IOException {
out.append("]}");//TODO:
}
public void writeRow(Object[] values) throws IOException {
out.append('{');//TODO:
for (int i = 0; i < cols.length; i++) {
out.append("\"" + cols[i] + "\":");//TODO:
String value;
if (values[i] instanceof Clob) {
Clob clob = (Clob) values[i];
try {
value = clob.getSubString(1, (int) clob.length());
} catch (SQLException e) {
throw new RuntimeException(e);
}
} else {
value = String.valueOf(values[i]);
}
out.append("\"" + sanitise(value) + "\""); //TODO:
if (i < cols.length - 1) out.append(','); //TODO:
}
out.append('}'); //TODO:
}
public void writeRowSeparator() throws IOException {
out.append(",\n"); //TODO:
}
public void writeUpdateCount(int updateCount) throws IOException {
out.append(String.valueOf(updateCount)); //TODO:
}
public static String sanitise(String s) {
// escape backslash, double quotes and newlines
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
}
}
/******************************************************************************
* TODO:WQM : should be factored out when WQM is not dependant of Servelt code anymore.
*Changed from type from Servletresponse to StringBuffer
*****************************************************************************/
class BlobResultWriter implements ResultWriter {
// Use PROPRIETARY notice if class contains a main() method, otherwise use COPYRIGHT notice.
public static final String COPYRIGHT_NOTICE = "(c) Copyright IBM Corp. 2011";
protected StringBuffer response;
protected final String TYPE = "blob/clob"; // Just used for error messages
protected static final int BUFFER_SIZE = 1024 * 4; // 4KB
private static final String DATA_COL = "data";
private static final String CONTENT_TYPE_COL = "content_type";
private static final String FILENAME_COL = "filename";
protected int dataIndex = -1;
protected int contentTypeIndex = -1;
protected int filenameIndex = -1;
public BlobResultWriter(StringBuffer response) {
this.response = response;
}
public void writeHeader(String[] cols) throws Exception {
for (int i = 0; i < cols.length; i++) {
String colname=cols[i].toLowerCase();
if (colname.equals(DATA_COL)) dataIndex = i;
if (colname.equals(CONTENT_TYPE_COL)) contentTypeIndex = i;
if (colname.equals(FILENAME_COL)) filenameIndex = i;
}
// Data column must be present
if (dataIndex == -1) {
throw new Exception("DATA column must be present for " + TYPE + " query");
}
}
public void writeRow(Object[] values) throws IOException, Exception {
String contentType = getDefaultContentType();
if (contentTypeIndex != -1) {
contentType = (String) values[contentTypeIndex];
}
//TODO:WQM:response.setContentType(contentType);
response.append("Content-Type: "+contentType);
if (filenameIndex != -1) {
//TODO:WQM: response.setHeader("Content-disposition", "inline; " + "filename=\"" + values[filenameIndex] + "\"");
response.append("Content-disposition: "+"inline; " + "filename=\"" + values[filenameIndex] + "\"");
}
writeObject(values[dataIndex]);
}
protected void writeObject(Object o) throws Exception {
Blob blob = (Blob) o;
// Commenting out the unused InputStream until this is rewritten,
// as noted just below.
//InputStream in;
try {
//in = blob.getBinaryStream();
blob.getBinaryStream();
} catch (SQLException e) {
throw new RuntimeException(e);
}
//TODO: To be rewritten properly. need to investigate !!!
throw new Exception("Writing BLOB on GDB WWWW Server is not yet supported!");
//TODO: copyBinaryData(in, response.getOutputStream());
}
public void writeRowSeparator() throws IOException, Exception {
// There should only ever be one row written
//TODO:WQM: throw new ServletException("Cannot return multiple " + TYPE + "s from 1 query");
throw new Exception("Cannot return multiple " + TYPE + "s from 1 query");
}
public void writeFooter() throws IOException {
// Do nothing
}
public void writeUpdateCount(int updateCount) throws IOException, Exception {
//TODO:WQM: throw new ServletException("Broken query: should not use " + TYPE + " response format unless the query returns a ResultSet");
throw new Exception("Broken query: should not use " + TYPE + " response format unless the query returns a ResultSet");
}
protected static void copyBinaryData (InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
int byteCount;
while ( (byteCount = in.read(buffer)) != -1 ) {
out.write(buffer, 0, byteCount);
}
}
protected String getDefaultContentType() {
return "application/octet-stream";
}
}