/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program 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 version 2 of the License.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.core.db;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import mazz.i18n.Logger;
import mazz.i18n.Msg;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* This class performs XML parsing on content that defines database specific type mappings. This is how you define the
* type names that are valid for the different database vendors. A type map has one or more typemap nodes - a typemap
* defines a generic type (e.g. LONG, BOOLEAN) and a set of database specific types. You define a "generic" type name
* (the <i>typemap</i> element's <i>type</i> attribute) and you give a database vendor mapping that defines the
* database-specific type that is the most appropriate for that generic type. Here's an example for the generic type
* "LONG":
*
* <pre>
* <typemaps type="LONG">
* <typemap type="LONG">
* <map db="java" type="BIGINT" />
* <map db="postgresql" type="BIGINT" />
* <map db="oracle" type="NUMBER(19,0)" />
* </typemap>
* </typemaps>
* </pre>
*
* <p>The database name of "java" represents JDBC SQL types. The other database names are those that are returned by the
* different implementations of {@link DatabaseType#getName()}.</p>
*
* <p>Instances of this object are created via {@link #loadTypeMapsFromStream(InputStream)} and
* {@link #loadKnownTypeMaps()}.</p>
*/
public class TypeMap {
private static final Logger LOG = DbUtilsI18NFactory.getLogger(TypeMap.class);
private static final Msg MSG = DbUtilsI18NFactory.getMsg();
/**
* The XML file name containing the type maps of the known DB types. This should be found in this class's
* classloader.
*/
private static final String DB_TYPEMAPS_XML_FILENAME = "db-typemaps.xml";
/**
* XML element name that maps a generic type to a set of database specific types.
*/
private static final String TYPEMAP_ELEMENT = "typemap";
/**
* The subelement name that is a child of the {@link #TYPEMAP_ELEMENT} element. Its children will define a single
* mapping for a database specific type.
*/
private static final String TYPEMAP_MAP_ELEMENT = "map";
/**
* The attribute of the {@link #TYPEMAP_ELEMENT} element that indicates the specific database the mapping is for.
*/
private static final String DB_ATTRIBUTE = "db";
/**
* The attribute of either the {@link #TYPEMAP_ELEMENT} or the {@link #TYPEMAP_MAP_ELEMENT} element that indicates
* the generic or database specific type name, respectively.
*/
private static final String TYPE_ATTRIBUTE = "type";
/**
* Represents the non-database-specific map type; types mapped to this are JDBC SQL types.
*/
private static final String JAVA_DATABASE_TYPE = "java";
/**
* The generic name for the type this type map is defining. The mappings of this typemap will define the database
* specific type that correlates to this generic, common type name.
*/
private String m_genericTypeName;
/**
* Contains the database specific type map which maps the different databases and their respective types that
* correlate to the generic type.
*/
private Hashtable<String, String> m_databaseTypeMap = new Hashtable<String, String>();
/**
* A collection of types that this class already knows about.
*/
private static Collection<TypeMap> m_knownTypes;
/**
* Given a DOM node, this will ensure it is a valid <i>typemap</i> node and parses it. This constructor is private,
* use {@link #loadKnownTypeMaps()} TypeMapsFromFile(File)} or {@link #loadTypeMapsFromStream(java.io.InputStream)}
* to create instances of this object.
*
* @param typemap_node the top-level <i>typemap</i> DOM node
*
* @throws SAXException if <code>node</code> is not a valid typemap node
*/
private TypeMap(Node typemap_node) throws SAXException {
if (TypeMap.isTypeMap(typemap_node)) {
NamedNodeMap typemap_node_attribs = typemap_node.getAttributes();
// there really should only be one attrib, but loop through looking for the one type attrib we want
for (int i_typemap_node_attrib = 0; i_typemap_node_attrib < typemap_node_attribs.getLength(); i_typemap_node_attrib++) {
Node typemap_attrib = typemap_node_attribs.item(i_typemap_node_attrib);
String typemap_attrib_name = typemap_attrib.getNodeName();
String typemap_attrib_value = typemap_attrib.getNodeValue();
// if this is the type attrib we want, then its value is the generic type being defined
if (TYPE_ATTRIBUTE.equalsIgnoreCase(typemap_attrib_name)) {
this.m_genericTypeName = typemap_attrib_value;
// each child node of the typemap is one database-specific type mapping
NodeList maps = typemap_node.getChildNodes();
for (int i_map = 0; i_map < maps.getLength(); i_map++) {
Node typemap_map_node = maps.item(i_map);
// ignore text/comment nodes
if (typemap_map_node.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
if (TYPEMAP_MAP_ELEMENT.equalsIgnoreCase(typemap_map_node.getNodeName())) {
String db = null;
String db_type = null;
NamedNodeMap map_attribs = typemap_map_node.getAttributes();
for (int i_attr = 0; i_attr < map_attribs.getLength(); i_attr++) {
Node typemap_map_attrib = map_attribs.item(i_attr);
typemap_attrib_name = typemap_map_attrib.getNodeName();
typemap_attrib_value = typemap_map_attrib.getNodeValue();
if (DB_ATTRIBUTE.equalsIgnoreCase(typemap_attrib_name)) {
db = typemap_attrib_value;
} else if (TYPE_ATTRIBUTE.equalsIgnoreCase(typemap_attrib_name)) {
db_type = typemap_attrib_value;
} else {
throw new SAXException(MSG.getMsg(DbUtilsI18NResourceKeys.INVALID_TYPE_MAP_ATTRIB,
this.m_genericTypeName, typemap_attrib_name, typemap_attrib_value));
}
}
this.m_databaseTypeMap.put(db, db_type);
} else {
throw new SAXException(MSG.getMsg(DbUtilsI18NResourceKeys.INVALID_TYPE_MAP_CHILD,
typemap_map_node.getNodeName(), TYPEMAP_MAP_ELEMENT));
}
}
}
}
} else {
throw new SAXException(MSG.getMsg(DbUtilsI18NResourceKeys.NODE_NOT_VALID_TYPEMAP_NODE, typemap_node));
}
if (m_genericTypeName == null) {
throw new SAXException(MSG.getMsg(DbUtilsI18NResourceKeys.MISSING_TYPE_MAP_GENERIC_TYPE, TYPE_ATTRIBUTE));
}
return;
}
/**
* If this type map defines database types for the given generic type, this returns the type for the given database.
* If this type mapping does not define database specific types for the given generic type or it doesn't have a type
* defined for the given database, <code>null</code> is returned.
*
* <p>The database name (which is unique to a vendor/version) is first used to look up the type. If none is found,
* simply the vendor name is used to look up the type. This allows the XML to define a default type for all versions
* of a specific database vendor; but if a type needs to be different for a specific version of the vendor database,
* this allows for that.</p>
*
* If <code>database</code> is <code>null</code>, this means you are asking for a non-vendor-specific map type; in
* other words, the JDBC SQL type.
*
* @param generic_type
* @param database
*
* @return the database specific type that is to be used for the given generic type on the given database; will be
* <code>null</code> if the type is not found within this object.
*/
public String getMappedType(String generic_type, DatabaseType database) {
String db_specific_type = null;
if (m_genericTypeName.equalsIgnoreCase(generic_type)) {
if (database == null) {
db_specific_type = m_databaseTypeMap.get(JAVA_DATABASE_TYPE);
} else {
db_specific_type = m_databaseTypeMap.get(database.getName());
// there is no vendor/version specific type, see if one is defined for all versions of the database vendor
if (db_specific_type == null) {
db_specific_type = m_databaseTypeMap.get(database.getVendor());
}
}
}
return db_specific_type;
}
/**
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder str = new StringBuilder(m_genericTypeName);
str.append('=');
str.append(m_databaseTypeMap);
return str.toString();
}
/**
* Given a collection of generic type maps, this returns the database specific type for the given generic type. If
* <code>database</code> is <code>null</code>, this means you are asking for a non-vendor-specific map type; in
* other words, the JDBC SQL type.
*
* @param typemaps collection of mappings for multiple generic types
* @param generic_type the generic type name whose corresponding database specific type is to be returned
* @param database
*
* @return returns the database specific type that maps to the given generic type; <code>null</code> is returned if
* there is no database specific mapping
*/
public static String getMappedType(Collection<TypeMap> typemaps, String generic_type, DatabaseType database) {
for (TypeMap typemap : typemaps) {
String type_name = typemap.getMappedType(generic_type, database);
if (type_name != null) {
return type_name;
}
}
return null;
}
/**
* Tests to see if the given node is a valid {@link #TYPEMAP_ELEMENT} node. A valid typemap node consists of the
* parent node that defines the generic type name and has children that defines all the database specific types.
*
* @param node_to_test the suspect typemap node
*
* @return <code>true</code> if the given node is a typemap node.
*/
private static boolean isTypeMap(Node node_to_test) {
String node_name = node_to_test.getNodeName();
return TYPEMAP_ELEMENT.equalsIgnoreCase(node_name);
}
/**
* Given a typemaps node, this checks all its direct children and those that are a {@link #TYPEMAP_ELEMENT} node
* will be parsed and their children typemaps are parsed and returned.
*
* @param typemaps_node root node that contains one or more typemaps node children
*
* @return the collection of all the generic types which contain their database specific type mappings
*
* @throws SAXException if an invalid type mapping was encountered
*/
private static Collection<TypeMap> readTypeMaps(Node typemaps_node) throws SAXException {
Collection<TypeMap> result = new ArrayList<TypeMap>();
NodeList typemap_nodes = typemaps_node.getChildNodes();
for (int j = 0; j < typemap_nodes.getLength(); j++) {
try {
Node node = typemap_nodes.item(j);
// ignore text/comment nodes
if (node.getNodeType() == Node.ELEMENT_NODE) {
result.add(new TypeMap(node));
}
} catch (SAXException e) {
throw new SAXException(MSG.getMsg(DbUtilsI18NResourceKeys.INVALID_TYPE_MAP, e));
}
}
return result;
}
/**
* Loads in the known type maps. These type mappings are predefined in an XML file that ships with the code. The
* known types, once loaded, are cached in this object - calling this method afterwards will not re-parse the XML
* file, instead the cached type map collection will be returned.
*
* @return a collection of known type maps
*
* @throws IllegalStateException if the known XML file could not be loaded (should never occur) or was invalid
*/
public static Collection<TypeMap> loadKnownTypeMaps() {
if (m_knownTypes == null) {
InputStream stream = TypeMap.class.getClassLoader().getResourceAsStream(DB_TYPEMAPS_XML_FILENAME);
if (stream == null) {
// this should not happen - this file should ship in the jar with this class
throw new IllegalStateException(MSG.getMsg(DbUtilsI18NResourceKeys.KNOWN_TYPEMAPS_XML_FILE_NOT_FOUND,
DB_TYPEMAPS_XML_FILENAME));
}
try {
m_knownTypes = loadTypeMapsFromStream(stream);
} catch (Exception e) {
// this should never happen - we should never ship an invalid set of default types
throw new IllegalStateException(e);
}
}
return m_knownTypes;
}
/**
* Given an input stream that contains an XML file of type maps, this will load in those type maps.
*
* @param xml_stream the stream containing the XML mapping (must not be <code>null</code>)
*
* @return a collection of type maps as found in the given typemaps XML stream
*
* @throws NullPointerException if <code>xml_stream</code> is <code>null</code>
* @throws Exception if the type maps data was invalid
*/
public static Collection<TypeMap> loadTypeMapsFromStream(InputStream xml_stream) throws Exception {
if (xml_stream == null) {
throw new NullPointerException("xml_stream == null");
}
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(xml_stream);
Collection<TypeMap> typemaps = readTypeMaps(doc.getDocumentElement());
LOG.debug(DbUtilsI18NResourceKeys.LOADED_TYPE_MAPS, typemaps);
return typemaps;
}
}