View Javadoc

1   /*$Id: AdqlSqlMaker.java,v 1.3 2006/06/15 16:50:09 clq2 Exp $
2    * Created on 27-Nov-2003
3    *
4    * Copyright (C) AstroGrid. All rights reserved.
5    *
6    * This software is published under the terms of the AstroGrid
7    * Software License version 1.2, a copy of which has been included
8    * with this distribution in the LICENSE.txt file.
9    *
10   **/
11  package org.astrogrid.tableserver.jdbc;
12  
13  import java.io.FileInputStream;
14  import java.io.File;
15  import java.io.IOException;
16  import java.io.FileNotFoundException;
17  import java.io.InputStream;
18  import java.io.StringWriter;
19  import javax.xml.parsers.ParserConfigurationException;
20  import javax.xml.transform.Transformer;
21  import javax.xml.transform.TransformerConfigurationException;
22  import javax.xml.transform.TransformerException;
23  import javax.xml.transform.TransformerFactory;
24  import javax.xml.transform.dom.DOMSource;
25  import javax.xml.transform.stream.StreamResult;
26  import javax.xml.transform.stream.StreamSource;
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  import org.astrogrid.cfg.ConfigFactory;
30  import org.astrogrid.query.Query;
31  import org.astrogrid.query.QueryException;
32  import org.astrogrid.xml.DomHelper;
33  import org.w3c.dom.Document;
34  import org.w3c.dom.Element;
35  import org.xml.sax.SAXException;
36  
37  /***
38   * A translator that extracts SQL from a query. 
39   * This uses functionality supplied by the Query instance to translate itself
40   * to SQL, using a stylesheet specified by a DSA configuration property.
41   * The AdqlSqlMaker should therefore be a fairly generic RDBMS plugin,
42   * flavoured for particular systems by choice of stylesheet.
43   *
44   * For weird RDBMS SQL flavours (where extra tweaking is required that 
45   * can't be done in XSLT), this class can be subclassed.
46   *
47   * @author M Hill
48   * @author K Andrews
49   */
50  public class AdqlSqlMaker implements SqlMaker {
51  
52     private static final Log log = LogFactory.getLog(AdqlSqlMaker.class);
53     
54     /***
55      * Constructs an SQL statement for the given Query.  Uses the ADQL generator
56      * and XSLT style sheets - there may be a better way of doing this!
57      */
58     public String makeSql(Query query) throws QueryException {
59       // Descendant classes might do something extra in here
60        String sql = doXsltTransformation(query);
61       // Or in here
62       return sql;
63     }
64  
65     protected String doXsltTransformation(Query query) throws QueryException
66     {
67        InputStream xsltIn = null;
68        String key = "datacenter.sqlmaker.xslt";
69        String whereIsDoc = "";
70  
71        String xsltDoc = ConfigFactory.getCommonConfig().getString(key, null);
72        if ((xsltDoc == null) || (xsltDoc.trim().equals(""))) {
73            throw new QueryException(
74                "Property 'datacenter.sqlmaker.xslt' is not configured,"+
75                " cannot translate to sql.");
76        }
77  
78        // First try to find file directly (in case it's a filesystem file)
79        log.debug("Trying to load XSLT as filesystem file: " + xsltDoc); 
80        File file = new File(xsltDoc);
81        if (file.exists()) {
82          try {
83            xsltIn = new FileInputStream(xsltDoc);
84          }
85          catch (FileNotFoundException e) {
86          }
87        }
88  
89        if (xsltIn == null) {
90           //find specified sheet as resource of this class
91           xsltIn = AdqlSqlMaker.class.getResourceAsStream("./xslt/"+xsltDoc);
92           whereIsDoc = AdqlSqlMaker.class+" resource ./xslt/"+xsltDoc;
93           log.debug("Trying to load XSLT as resource of class: " +whereIsDoc);
94        }
95  
96        //if above doesn't work, try doing by hand for Tomcat ClassLoader
97        if (xsltIn == null) {
98           // Extract the package name, turn it into a filesystem path by 
99           // replacing .'s with /'s, and append the path to the xslt.
100          // Hopefully this will mean tomcat can locate the file within
101          // the bundled jar file.  
102          // NB: Comment below about tomcat not finding it via this method
103          // *may* be incorrect - previously this bit of code used package's
104          // toString() method, not getName() method, which added
105          // extra stuff like package description into the returned string
106          // and thereby messed up the file path conversion. 
107          String path = 
108            AdqlSqlMaker.class.getPackage().getName().replace('.', '/') +
109                "/xslt/" + xsltDoc;
110          xsltIn = AdqlSqlMaker.class.getClassLoader().getResourceAsStream(path);
111          log.debug("Trying to load XSLT via classloader : " +path);
112       }
113 
114       //Sometimes it won't even find it then if it's in a JAR.  
115       //Look in class path.  However *assume* it's in classpath, 
116       //as we don't know what the classpath is during unit tests.
117       if (xsltIn == null) {
118          log.warn("Could not find builtin ADQL->SQL transformer doc '"
119              +whereIsDoc+"', looking in classpath...");
120 
121          xsltIn = this.getClass().getClassLoader().getResourceAsStream(
122                 "xslt/"+xsltDoc);
123          whereIsDoc = "xslt/" + xsltDoc+" in classpath at "+
124            this.getClass().getClassLoader().getResource(xsltDoc);
125          log.debug("Trying to load XSLT via classpath : " +whereIsDoc);
126       }
127 
128       if (xsltIn == null) {
129           throw new QueryException(
130               "Could not find ADQL->SQL transformer doc "+xsltDoc);
131       }
132 
133       //use config-specified sheet
134       /*
135       FileInputStream xsltIn;
136       try {
137          xsltIn = new FileInputStream(xsltDoc);
138       }
139       catch (java.io.FileNotFoundException e) {
140           throw new QueryException(
141               "Unable to open stylesheet file '" + xsltDoc +
142               "', cannot translate to sql.");
143       }
144       */
145 
146       // Now get the query to convert itself with the stylesheet
147       return query.convertWithXslt(xsltIn);
148    }
149 
150    /***
151     * Constructs an SQL count statement for the given Query.
152     */
153 
154    /***
155     * Constructs an SQL count statement for the given Query.
156     * @TOFIX KEA SAYS: This is a truly disgusting hack which won't
157     * work with ADQL queries with complex expression selections as 
158     * aliased columns.  It may also fail if "select" or "from" appears
159     * in unexpected places (e.g. as part of column / table names).
160     * However, this method is intended for administrative/testing 
161     * use only (I'm going to remove it from the public Datacenter
162     * API) so I will leave it here for now.
163     * @deprecated Because it's horrible/fragile
164     */
165    public String makeCountSql(Query query) throws QueryException {
166 
167       //get ordinary SQL
168       String sql = makeSql(query);
169       String lowercaseSql = sql.toLowerCase();
170       
171       //remove anything between SELECT and FROM and replace with COUNT(*)
172       // Use lowercased sql to find indices
173       int selectIdx = lowercaseSql.indexOf("select");
174       int fromIdx = lowercaseSql.indexOf("from");
175       String countSql = sql.substring(0,selectIdx+6)+" COUNT(*) "+sql.substring(fromIdx);
176       return countSql;
177    }
178    /*
179     * This version will count the rows in a table; it's likely
180     * to be more reliable, but doesn't reflect the original query constraints.
181    public String makeCountSql(Query query) throws QueryException {
182 
183       String tableNames[] = query.getTableReferences();
184       if (tableNames.length == 0) {
185          // This should never happen, as Query class creates a default
186          // FROM table if one is not given in the input ADQL.
187          throw new QueryException(
188            "Unexpected error - query contained no table references!");
189       }
190       if (tableNames.length > 1) {
191          throw new QueryException(
192           "Cannot produce count(*) SQL from query referencing multiple tables");
193       }
194       // Once we get here, we have 1 table name only
195       String countSql = "SELECT COUNT(*) FROM " + tableNames[0] + ";";
196       return countSql;
197    }
198    */
199 }