/** * Copyright (C) 2004 Orbeon, Inc. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation; either version * 2.1 of the License, or (at your option) any later version. * * This program 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 Lesser General Public License for more details. * * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html */ package org.orbeon.oxf.processor.sql.interpreters; import org.joda.time.format.ISODateTimeFormat; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.common.ValidationException; import org.orbeon.oxf.processor.sql.SQLProcessor; import org.orbeon.oxf.processor.sql.SQLProcessorInterpreterContext; import org.orbeon.oxf.properties.PropertySet; import org.orbeon.oxf.util.DateUtils; import org.orbeon.oxf.xml.SAXUtils; import org.orbeon.oxf.xml.TransformerUtils; import org.orbeon.oxf.xml.XMLConstants; import org.orbeon.oxf.xml.XMLParsing; import org.orbeon.oxf.xml.dom4j.Dom4jUtils; import org.orbeon.oxf.xml.dom4j.LocationData; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.helpers.NamespaceSupport; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXResult; import java.io.InputStream; import java.io.Reader; import java.math.BigDecimal; import java.sql.*; import java.util.HashMap; import java.util.Map; /** * */ public class GetterInterpreter extends SQLProcessor.InterpreterContentHandler { private ResultSetMetaData metadata; private int getColumnsLevel; private String getColumnsFormat; private String getColumnsPrefix; private boolean getColumnsAllElements; private boolean inExclude; private StringBuilder getColumnsCurrentExclude; private Map getColumnsExcludes; public GetterInterpreter(SQLProcessorInterpreterContext interpreterContext) { super(interpreterContext, false); } public void characters(char[] chars, int start, int length) throws SAXException { if (inExclude) getColumnsCurrentExclude.append(chars, start, length); else super.characters(chars, start, length); } public void start(String uri, String localname, String qName, Attributes attributes) throws SAXException { final SQLProcessorInterpreterContext interpreterContext = getInterpreterContext(); interpreterContext.getNamespaceSupport().pushContext(); try { final String levelString = attributes.getValue("ancestor"); final int level = (levelString == null) ? 0 : Integer.parseInt(levelString); final ResultSet resultSet = interpreterContext.getResultSet(level); metadata = resultSet.getMetaData(); if ("get-columns".equals(localname)) { // Remember attributes getColumnsLevel = level; getColumnsFormat = attributes.getValue("format"); getColumnsPrefix = attributes.getValue("prefix"); getColumnsAllElements = "true".equals(attributes.getValue("all-elements")); } else if ("get-column".equals(localname) || "get-column-value".equals(localname)) { final String columnName; { final String columnNameAttribute = attributes.getValue("column-name"); final String columnAttribute = attributes.getValue("column"); if (columnNameAttribute != null) columnName = columnNameAttribute; else if (columnAttribute != null) columnName = columnAttribute; else columnName = interpreterContext.getColumnName(); } final int columnIndex = resultSet.findColumn(columnName); final int columnType = metadata.getColumnType(columnIndex); final String xmlType = getXMLTypeFromAttributeStringHandleDefault(getDocumentLocator(), interpreterContext.getPropertySet(), attributes.getValue("type"), interpreterContext.getPrefixesMap(), columnType); if (Dom4jUtils.qNameToExplodedQName(XMLConstants.OPS_XMLFRAGMENT_QNAME).equals(xmlType)) { // XML fragment requested String columnTypeName = metadata.getColumnTypeName(columnIndex); if (columnType == Types.CLOB) { // The fragment is stored as a Clob Clob clob = resultSet.getClob(columnName); if (clob != null) { Reader reader = clob.getCharacterStream(); try { XMLParsing.parseDocumentFragment(reader, interpreterContext.getOutput()); } finally { reader.close(); } } } else if (interpreterContext.getDelegate().isXMLType(columnType, columnTypeName)) { // The fragment is stored as a native XMLType org.w3c.dom.Node node = interpreterContext.getDelegate().getDOM(resultSet, columnName); // NOTE: XML comments not supported yet if (node != null) { TransformerUtils.getIdentityTransformer().transform(new DOMSource(node), new SAXResult(new SQLProcessor.ForwardingContentHandler() { protected ContentHandler getContentHandler() { return interpreterContext.getOutput(); } public void endDocument() { } public void startDocument() { } })); } } else { // The fragment is stored as a String String value = resultSet.getString(columnName); if (value != null) XMLParsing.parseDocumentFragment(value, interpreterContext.getOutput()); } } else { // xs:* Object o = getColumnValue(resultSet, getDocumentLocator(), columnIndex, xmlType); if (o != null) { if (o instanceof Clob) { Reader reader = ((Clob) o).getCharacterStream(); try { SAXUtils.readerToCharacters(reader, interpreterContext.getOutput()); } finally { reader.close(); } } else if (o instanceof Blob) { InputStream is = ((Blob) o).getBinaryStream(); try { SAXUtils.inputStreamToBase64Characters(is, interpreterContext.getOutput()); } finally { is.close(); } } else if (o instanceof InputStream) { InputStream is = (InputStream) o; try { SAXUtils.inputStreamToBase64Characters(is, interpreterContext.getOutput()); } finally { is.close(); } } else { SAXUtils.objectToCharacters(o, interpreterContext.getOutput()); } } } } else if ("get-column-name".equals(localname)) { final String columnName = attributes.getValue("column-name"); final String columnIndex = attributes.getValue("column-index"); final String result; if (columnName == null && columnIndex == null) { // Get from context result = interpreterContext.getColumnName(); } else if (columnName != null){ // Trivial case result = columnName; } else { // Get name from index // NOTE: getColumnLabel() allows SQL "AS" to work result = metadata.getColumnLabel(Integer.parseInt(columnIndex)); } final char[] charResult = result.toCharArray(); interpreterContext.getOutput().characters(charResult, 0, charResult.length); } else if ("get-column-index".equals(localname)) { final String columnName = attributes.getValue("column-name"); final String columnIndex = attributes.getValue("column-index"); final String result; if (columnName == null && columnIndex == null) { // Get from context result = Integer.toString(interpreterContext.getColumnIndex()); } else if (columnName != null){ // Get index from name result = Integer.toString(resultSet.findColumn(columnName)); } else { // Trivial case result = columnIndex; } final char[] charResult = result.toCharArray(); interpreterContext.getOutput().characters(charResult, 0, charResult.length); } else if ("get-column-type".equals(localname)) { final String columnName = attributes.getValue("column-name"); final String columnIndex = attributes.getValue("column-index"); final String result; if (columnName == null && columnIndex == null) { // Get from context result = interpreterContext.getColumnType(); } else if (columnName != null){ // result = metadata.getColumnTypeName(resultSet.findColumn(columnName)); } else { // Get type from index result = metadata.getColumnTypeName(Integer.parseInt(columnIndex)); } final char[] charResult = result.toCharArray(); interpreterContext.getOutput().characters(charResult, 0, charResult.length); } else { // Simple getter (deprecated) final String columnName = (attributes.getValue("column-name") != null) ? attributes.getValue("column-name") : attributes.getValue("column"); final int columnIndex = resultSet.findColumn(columnName); final Object o = getColumnValue(resultSet, getDocumentLocator(), columnIndex, getXMLTypeFromLegacyGetterName(localname)); if (o != null) { if (o instanceof Clob) { final Reader reader = ((Clob) o).getCharacterStream(); try { SAXUtils.readerToCharacters(reader, interpreterContext.getOutput()); } finally { reader.close(); } } else if (o instanceof Blob) { final InputStream is = ((Blob) o).getBinaryStream(); try { SAXUtils.inputStreamToBase64Characters(is, interpreterContext.getOutput()); } finally { is.close(); } } else if (o instanceof InputStream) { final InputStream is = (InputStream) o; try { SAXUtils.inputStreamToBase64Characters(is, interpreterContext.getOutput()); } finally { is.close(); } } else { SAXUtils.objectToCharacters(o, interpreterContext.getOutput()); } } } } catch (Exception e) { throw new ValidationException(e, new LocationData(getDocumentLocator())); } } public void startElement(String uri, String localname, String qName, Attributes attributes) throws SAXException { if ("exclude".equals(localname)) { // Collect excludes if (getColumnsExcludes == null) getColumnsExcludes = new HashMap(); getColumnsCurrentExclude = new StringBuilder(); inExclude = true; } } public void endElement(String uri, String localname, String qName) throws SAXException { if ("exclude".equals(localname)) { // Add current exclude String value = getColumnsCurrentExclude.toString().toLowerCase(); getColumnsExcludes.put(value, value); inExclude = false; } } public void end(String uri, String localname, String qName) throws SAXException { SQLProcessorInterpreterContext interpreterContext = getInterpreterContext(); try { if ("get-columns".equals(localname)) { // Do nothing except collect sql:exclude final ResultSet resultSet = interpreterContext.getResultSet(getColumnsLevel); if (metadata == null) metadata = resultSet.getMetaData(); NamespaceSupport namespaceSupport = interpreterContext.getNamespaceSupport(); // for (Enumeration e = namespaceSupport.getPrefixes(); e.hasMoreElements();) { // String p = (String) e.nextElement(); // String u = namespaceSupport.getURI(p); // System.out.println("Prefix: " + p + " -> " + u); // } // Get format once for all columns // Get URI once for all columns String outputElementURI = (getColumnsPrefix == null) ? "" : namespaceSupport.getURI(getColumnsPrefix); if (outputElementURI == null) throw new ValidationException("Invalid namespace prefix: " + getColumnsPrefix, new LocationData(getDocumentLocator())); // Iterate through all columns for (int i = 1; i <= metadata.getColumnCount(); i++) { // Get column name // NOTE: getColumnLabel() allows SQL "AS" to work String columnName = metadata.getColumnLabel(i); // Make sure it is not excluded if (getColumnsExcludes != null && getColumnsExcludes.get(columnName.toLowerCase()) != null) continue; // Process column int columnType = metadata.getColumnType(i); Clob clobValue = null; Blob blobValue = null; String stringValue = null; if (columnType == Types.CLOB) { clobValue = resultSet.getClob(i); } else if (columnType == Types.BLOB) { blobValue = resultSet.getBlob(i); } else { stringValue = getColumnStringValue(resultSet, i, columnType); } final boolean nonNullValue = stringValue != null || clobValue != null || blobValue != null; if (nonNullValue || getColumnsAllElements) { // Format element name String elementName = columnName; if ("xml".equals(getColumnsFormat)) { elementName = elementName.toLowerCase(); elementName = elementName.replace('_', '-'); } else if (getColumnsFormat != null) throw new ValidationException("Invalid get-columns format: " + getColumnsFormat, new LocationData(getDocumentLocator())); String elementQName = (outputElementURI.equals("")) ? elementName : getColumnsPrefix + ":" + elementName; ContentHandler output = interpreterContext.getOutput(); output.startElement(outputElementURI, elementName, elementQName, SAXUtils.EMPTY_ATTRIBUTES); // Output value if non-null if (nonNullValue) { if (clobValue == null && blobValue == null) { // Just output the String value as characters char[] localCharValue = stringValue.toCharArray(); output.characters(localCharValue, 0, localCharValue.length); } else if (clobValue != null) { // Clob: convert the Reader into characters Reader reader = clobValue.getCharacterStream(); try { SAXUtils.readerToCharacters(reader, output); } finally { reader.close(); } } else { // Blob: convert the InputStream into characters in Base64 InputStream is = blobValue.getBinaryStream(); try { SAXUtils.inputStreamToBase64Characters(is, output); } finally { is.close(); } } } output.endElement(outputElementURI, elementName, elementQName); } } } } catch (Exception e) { throw new ValidationException(e, new LocationData(getDocumentLocator())); } interpreterContext.getNamespaceSupport().popContext(); } // Mapping for legacy getters private static final Map getterToXMLType = new HashMap(); static { getterToXMLType.put("get-string", "{http://www.w3.org/2001/XMLSchema}string"); getterToXMLType.put("get-int", "{http://www.w3.org/2001/XMLSchema}int"); getterToXMLType.put("get-boolean", "{http://www.w3.org/2001/XMLSchema}boolean"); getterToXMLType.put("get-decimal", "{http://www.w3.org/2001/XMLSchema}decimal"); getterToXMLType.put("get-float", "{http://www.w3.org/2001/XMLSchema}float"); getterToXMLType.put("get-double", "{http://www.w3.org/2001/XMLSchema}double"); getterToXMLType.put("get-timestamp", "{http://www.w3.org/2001/XMLSchema}dateTime"); getterToXMLType.put("get-date", "{http://www.w3.org/2001/XMLSchema}date"); getterToXMLType.put("get-base64binary", "{http://www.w3.org/2001/XMLSchema}base64Binary"); } private static final Map sqlTypesToDefaultXMLTypes = new HashMap(); static { sqlTypesToDefaultXMLTypes.put(new Integer(Types.CHAR), "{http://www.w3.org/2001/XMLSchema}string"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.VARCHAR), "{http://www.w3.org/2001/XMLSchema}string"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.LONGVARCHAR), "{http://www.w3.org/2001/XMLSchema}string"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.NUMERIC), "{http://www.w3.org/2001/XMLSchema}decimal"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.DECIMAL), "{http://www.w3.org/2001/XMLSchema}decimal"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.BIT), "{http://www.w3.org/2001/XMLSchema}boolean"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.BOOLEAN), "{http://www.w3.org/2001/XMLSchema}boolean"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.TINYINT), "{http://www.w3.org/2001/XMLSchema}byte"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.SMALLINT), "{http://www.w3.org/2001/XMLSchema}short"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.INTEGER), "{http://www.w3.org/2001/XMLSchema}int"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.BIGINT), "{http://www.w3.org/2001/XMLSchema}long"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.REAL), "{http://www.w3.org/2001/XMLSchema}float"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.FLOAT), "{http://www.w3.org/2001/XMLSchema}double"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.DOUBLE), "{http://www.w3.org/2001/XMLSchema}double"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.BINARY), "{http://www.w3.org/2001/XMLSchema}base64Binary"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.VARBINARY), "{http://www.w3.org/2001/XMLSchema}base64Binary"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.LONGVARBINARY), "{http://www.w3.org/2001/XMLSchema}base64Binary"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.DATE), "{http://www.w3.org/2001/XMLSchema}date"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.TIME), "{http://www.w3.org/2001/XMLSchema}time"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.TIMESTAMP), "{http://www.w3.org/2001/XMLSchema}dateTime"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.CLOB), "{http://www.w3.org/2001/XMLSchema}string"); sqlTypesToDefaultXMLTypes.put(new Integer(Types.BLOB), "{http://www.w3.org/2001/XMLSchema}base64Binary"); // sqlTypesToDefaultXMLTypes.put(new Integer(Types.ARRAY), "{http://www.w3.org/2001/XMLSchema}"); // sqlTypesToDefaultXMLTypes.put(new Integer(Types.DISTINCT), "{http://www.w3.org/2001/XMLSchema}"); // sqlTypesToDefaultXMLTypes.put(new Integer(Types.STRUCT), "{http://www.w3.org/2001/XMLSchema}"); // sqlTypesToDefaultXMLTypes.put(new Integer(Types.REF), "{http://www.w3.org/2001/XMLSchema}"); // sqlTypesToDefaultXMLTypes.put(new Integer(Types.DATALINK), "{http://www.w3.org/2001/XMLSchema}"); // sqlTypesToDefaultXMLTypes.put(new Integer(Types.JAVA_OBJECT), "{http://www.w3.org/2001/XMLSchema}");` } // private static final Map xmlTypesToDefaultSQLTypes = new HashMap(); // static { // xmlTypesToDefaultSQLTypes.put("{http://www.w3.org/2001/XMLSchema}string", new Integer(Types.CHAR)); // // } /** * Return a Clob or Clob object or a String. */ public static Object getColumnValue(ResultSet resultSet, Locator locator, int columnIndex, String xmlTypeName) throws SQLException { try { final int columnType = resultSet.getMetaData().getColumnType(columnIndex); final String defaultXMLType = (String) sqlTypesToDefaultXMLTypes.get(new Integer(columnType)); if (xmlTypeName != null && !xmlTypeName.equals(defaultXMLType)) throw new ValidationException("Illegal XML type for SQL type: " + xmlTypeName + ", " + resultSet.getMetaData().getColumnTypeName(columnIndex), new LocationData(locator)); if (columnType == Types.CLOB) { // The actual column is a CLOB return resultSet.getClob(columnIndex); } else if (columnType == Types.BLOB) { // The actual column is a BLOB return resultSet.getBlob(columnIndex); } else if (columnType == Types.BINARY || columnType == Types.VARBINARY || columnType == Types.LONGVARBINARY) { // The actual column is binary return resultSet.getBinaryStream(columnIndex); } else { // The actual column is not a CLOB or BLOB, in which case we use regular ResultSet getters return getColumnStringValue(resultSet, columnIndex, columnType); } } catch (SQLException e) { throw new ValidationException("Exception while getting column: " + (resultSet.getMetaData().getColumnLabel(columnIndex)), e, new LocationData(locator)); } } /** * * Return the String value of a given column using the default XML type for the given SQL type. * * This is not meant to work with CLOB and BLOB types. */ public static String getColumnStringValue(ResultSet resultSet, int columnIndex, int columnType) throws SQLException { String stringValue = null; if (columnType == Types.DATE) { final Date value = resultSet.getDate(columnIndex); if (value != null) stringValue = ISODateTimeFormat.date().print(value.getTime()); } else if (columnType == Types.TIMESTAMP) { final Timestamp value = resultSet.getTimestamp(columnIndex); if (value != null) stringValue = DateUtils.DateTime().print(value.getTime()); } else if (columnType == Types.DECIMAL || columnType == Types.NUMERIC) { final BigDecimal value = resultSet.getBigDecimal(columnIndex); stringValue = (resultSet.wasNull()) ? null : value.toString(); } else if (columnType == Types.BOOLEAN) { final boolean value = resultSet.getBoolean(columnIndex); stringValue = (resultSet.wasNull()) ? null : (value ? "true" : "false"); } else if (columnType == Types.INTEGER || columnType == Types.SMALLINT || columnType == Types.TINYINT || columnType == Types.BIGINT) { final long value = resultSet.getLong(columnIndex); stringValue = (resultSet.wasNull()) ? null : Long.toString(value); } else if (columnType == Types.DOUBLE || columnType == Types.FLOAT || columnType == Types.REAL) { final double value = resultSet.getDouble(columnIndex); // For XPath 1.0, we have to get rid of the scientific notation stringValue = resultSet.wasNull() ? null : Util.removeScientificNotation(value); } else if (columnType == Types.CLOB) { throw new OXFException("Cannot get String value for CLOB type."); } else if (columnType == Types.BLOB) { throw new OXFException("Cannot get String value for BLOB type."); } else { // Assume the type is compatible with getString() stringValue = resultSet.getString(columnIndex); } return stringValue; } public static String getXMLTypeFromLegacyGetterName(String getterName) { return (String) getterToXMLType.get(getterName); } public static String getXMLTypeFromAttributeStringHandleDefault(Locator locator, PropertySet propertySet, String typeAttribute, Map prefixesMap, int columnType) { String xmlType; if (typeAttribute != null) { // User specified an XML type xmlType = getXMLTypeFromAttributeString(locator, propertySet, typeAttribute, prefixesMap); } else { // Get default XML type for SQL type xmlType = getDefaultXMLTypeFromSQLType(columnType); } return xmlType; } public static String getDefaultXMLTypeFromSQLType(int type) { return (String) sqlTypesToDefaultXMLTypes.get(new Integer(type)); } public static String getXMLTypeFromAttributeString(Locator locator, PropertySet propertySet, String typeAttribute, Map prefixesMap) { final int colonIndex = typeAttribute.indexOf(':'); if (colonIndex < 1) throw new ValidationException("Invalid column type:" + typeAttribute, new LocationData(locator)); final String typePrefix = typeAttribute.substring(0, colonIndex); final String typeLocalname = typeAttribute.substring(colonIndex + 1); final String typeURI; if (prefixesMap.get(typePrefix) == null && !(Boolean.TRUE.equals(propertySet.getBoolean("legacy-implicit-prefixes")))) { throw new ValidationException("Undeclared type prefix for type:" + typeAttribute, new LocationData(locator)); } else if (prefixesMap.get(typePrefix) == null) { // LEGACY BEHAVIOR: use implicit prefixes if (typePrefix.equals("xs")) typeURI = XMLConstants.XSD_URI; else if (typePrefix.equals("oxf")) typeURI = XMLConstants.OPS_TYPES_URI; else throw new ValidationException("Invalid type prefix for type:" + typeAttribute, new LocationData(locator)); } else { // NEW BEHAVIOR: use actual mappings typeURI = (String) prefixesMap.get(typePrefix); } return "{" + typeURI + "}" + typeLocalname; } }