/* * Copyright (c) 2008, SQL Power Group Inc. * * This file is part of SQL Power Library. * * SQL Power Library 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. * * SQL Power Library 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, see <http://www.gnu.org/licenses/>. */ /* * Created on Jun 28, 2005 * * This code belongs to SQL Power Group Inc. */ package ca.sqlpower.sql; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.sql.DatabaseMetaData; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.swing.event.UndoableEditEvent; import javax.swing.event.UndoableEditListener; import javax.swing.undo.AbstractUndoableEdit; import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; import org.apache.log4j.Logger; import ca.sqlpower.sqlobject.SQLIndex; import ca.sqlpower.sqlobject.SQLTypePhysicalProperties; import ca.sqlpower.sqlobject.SQLTypePhysicalProperties.SQLTypeConstraint; import ca.sqlpower.sqlobject.SQLTypePhysicalPropertiesProvider; import ca.sqlpower.sqlobject.SQLTypePhysicalPropertiesProvider.BasicSQLType; import ca.sqlpower.sqlobject.SQLTypePhysicalPropertiesProvider.PropertyType; import ca.sqlpower.sqlobject.UserDefinedSQLType; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; /** * The PlDotIni class represents the contents of a PL.INI file; despit * the name, this actually represents the master list of Data Source connections. * <p> * Note, there is some confusion about whether or not section name matching should * be case sensitive or not. Both approaches are taken in this class! Vive la difference. * <p> * <b>Warning:</b> this file only reads (and therefore writes) files with MS-DOS line endings, * regardless of platform, since the encoding of binary passwords * could result in a bare \n in the encryption... * @version $Id$ */ public class PlDotIni implements DataSourceCollection<SPDataSource> { public class AddDSTypeUndoableEdit extends AbstractUndoableEdit { private final JDBCDataSourceType type; public AddDSTypeUndoableEdit(JDBCDataSourceType type) { this.type = type; } @Override public void redo() throws CannotRedoException { super.redo(); fileSections.add(type); } @Override public void undo() throws CannotUndoException { super.undo(); fileSections.remove(type); } public JDBCDataSourceType getType() { return type; } } public class RemoveDSTypeUndoableEdit extends AbstractUndoableEdit { private final JDBCDataSourceType type; public RemoveDSTypeUndoableEdit(JDBCDataSourceType type) { this.type = type; } @Override public void redo() throws CannotRedoException { super.redo(); fileSections.remove(type); } @Override public void undo() throws CannotUndoException { super.undo(); fileSections.add(type); } public JDBCDataSourceType getType() { return type; } } /** * Boolean to control whether we autosave, to prevent calling it while we're already saving. */ private boolean dontAutoSave; /** * The list of Listeners to notify when a datasource is added or removed. */ List<DatabaseListChangeListener> listeners; private final List<UndoableEditListener> undoListeners = new ArrayList<UndoableEditListener>(); DatabaseListChangeListener saver = new DatabaseListChangeListener() { public void databaseAdded(DatabaseListChangeEvent e) { saveIfFileKnown(); } public void databaseRemoved(DatabaseListChangeEvent e) { saveIfFileKnown(); } private void saveIfFileKnown() { if (dontAutoSave) return; if (lastFileAccessed != null) { try { write(lastFileAccessed); } catch (IOException e) { logger.error("Error auto-saving PL.INI file", e); } } } }; /** * A URI that Mondrian XML schemas can be retrieved from. */ private final URI mondrianServerBaseURI; /** * Construct a PL.INI object, and set an Add Listener to save * the file when a database is added (bugid 1032). */ public PlDotIni() { this(null); } /** * Constructs a data source collection with a URI that defines where to retrieve * JDBC drivers from the server. */ public PlDotIni(URI serverBaseURI) { this(serverBaseURI, null); } /** * Constructs a data source collection with a URI that defines where to * retrieve JDBC drivers from the server and another URI that defines where * to retrieve Mondrian schemas from the server. * * @param serverBaseURI * A URI that JDBC drivers can be retrieved from the server with. * @param mondrianServerBaseURI * A URI that Mondrian XML schemas can be retrieved from the * server with. */ public PlDotIni(URI serverBaseURI, URI mondrianServerBaseURI) { listeners = new ArrayList<DatabaseListChangeListener>(); listeners.add(saver); this.serverBaseURI = serverBaseURI; this.mondrianServerBaseURI = mondrianServerBaseURI; } /** * The Section class represents a named section in the PL.INI file. * It has default visibility because the unit test needs to use it. */ static class Section { /** The name of this section (without the square brackets). */ private String name; /** * The contents of this section (part before the '=' is the key, and * the rest of the line is the value). */ private Map<String, String> properties; /** Creates a new section with the given name and no properties. */ public Section(String name) { this.name = name; this.properties = new LinkedHashMap<String, String>(); } /** * Puts a new property in this section, or replaces an existing * property which has the same key. Keys are case-sensitive. * * @param key The property's key * @param value The property's value * @return The old value of the property under this key, if one existed. */ public Object put(String key, String value) { return properties.put(key, value); } /** Returns the whole properties map. This is required when saving the PL.INI file. */ public Map<String, String> getPropertiesMap() { return properties; } /** Returns the name of this section. */ public String getName() { return name; } /** * Updates this section's contents to look like the given one. * Doesn't modify the name. */ public void merge(Section s) { // get rid of deleted properties properties.keySet().retainAll(s.properties.keySet()); properties.putAll(s.properties); } } private static final Logger logger = Logger.getLogger(PlDotIni.class); /** * The UUID of the special "Unknown" UserDefinedSQLType, as defined in the default_database_types.ini file */ private static final String unknownTypeUUID = "Unknown_UserDefinedSQLType"; /** * A list of Section and SPDataSource objects, in the order they appear in the file; * this List contains Mixed Content (both Section and SPDataSource) which is * A Very Bad Idea(tm) so it cannot be converted to Java 5 Generic Collection. */ private final List<Object> fileSections = new ArrayList<Object>(); /** * The time we last read the PL.INI file. */ private long fileTime; /** * Base URI for server: JAR spec lookups. */ private URI serverBaseURI; boolean shuttingDown = false; /** Seconds to wait between checking the file. */ int WAIT_TIME = 30; /** * Thread to stat file periodically, reload if PL changed it. * FIXME This thread is not currently started! */ Thread monitor = new Thread() { public void run() { while (!shuttingDown) { try { Thread.sleep(WAIT_TIME * 1000); if (lastFileAccessed == null) { continue; } long newFileTime = lastFileAccessed.lastModified(); if (newFileTime != fileTime) { logger.debug("Re-reading PL.INI file because it has been modified externally."); read(lastFileAccessed); fileTime = newFileTime; } } catch (Exception e) { e.printStackTrace(); } } } }; File lastFileAccessed; /** * Returns the requested section in this pl.ini instance. The sections * are stored in the same order they appear in the file, but of course * there may have been sections added or removed since the file was * last read. * <p> * Note, this method is really only intended for testing purposes. * * @return The returned object will be of type Section, SPDataSource, * or SPDataSourceType depending on which type of file section it * represents. */ Object getSection(int number) { return fileSections.get(number); } /** * Returns the number of sections that were in the pl.ini file we read * (additions and deletions after reading the file will affect the count * of course). This will include unrecognized sections and the nameless * section at the top of the file, so don't expect the count to be equal * to the number of database types plus the number of connections. */ int getSectionCount() { return fileSections.size(); } /** * The enumeration of states that the read() method's INI file parser can be in. */ private enum ReadState {READ_DS, READ_GENERIC, READ_TYPE, READ_SQLTYPE} public void read(File location) throws IOException { if (!location.canRead()) { throw new IOException("pl.ini file is not readable: " + location.getAbsolutePath()); } fileTime = location.lastModified(); lastFileAccessed = location; read(new FileInputStream(location)); } public void read(InputStream inStream) throws IOException { if (inStream == null) throw new NullPointerException("InputStream was null!"); logger.info("Beginning to read/merge new pl.ini data"); ReadState mode = ReadState.READ_GENERIC; try { dontAutoSave = true; JDBCDataSourceType currentType = null; SPDataSource currentDS = null; UserDefinedSQLType currentSQLType = null; Section currentSection = new Section(null); // this accounts for any properties before the first named section Multimap<String, SQLTypePhysicalProperties> typePropertiesMap = ArrayListMultimap.create(); // Can't use Reader to read this file because the encrypted passwords contain non-ASCII characters BufferedInputStream in = new BufferedInputStream(inStream); byte[] lineBytes = null; while ((lineBytes = readLine(in)) != null) { String line = new String(lineBytes); line = convertOldLines(line); logger.debug("Read in new line: "+line); if (line.startsWith("[")) { mergeFileData(mode, currentType, currentDS, currentSQLType, currentSection); } if (line.startsWith("[Databases_")) { logger.debug("It's a new database connection spec!" +fileSections); currentDS = new JDBCDataSource(this); mode = ReadState.READ_DS; } else if (line.startsWith("[OLAP_databases_")) { logger.debug("It's a new database connection spec!" +fileSections); currentDS = new Olap4jDataSource(this); mode = ReadState.READ_DS; } else if (line.startsWith("[Database Types_")) { logger.debug("It's a new database type!" + fileSections); currentType = new JDBCDataSourceType(getServerBaseURI()); mode = ReadState.READ_TYPE; } else if (line.startsWith("[Data Types_")) { logger.debug("It's a new data type!" + fileSections); currentSQLType = new UserDefinedSQLType(); String platform = SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM; currentSQLType.setConstraintType(platform, SQLTypeConstraint.NONE); currentSQLType.setDefaultValue(platform, ""); currentSQLType.setPrecision(platform, 0); currentSQLType.setScale(platform, 0); currentSQLType.setMyAutoIncrement(false); currentSQLType.setMyNullability(Integer.valueOf(DatabaseMetaData.columnNoNulls)); mode = ReadState.READ_SQLTYPE; } else if (line.startsWith("[")) { logger.debug("It's a new generic section!"); currentSection = new Section(line.substring(1, line.length()-1)); mode = ReadState.READ_GENERIC; } else { String key; String value; int equalsIdx = line.indexOf('='); if (equalsIdx > 0) { key = line.substring(0, equalsIdx); value = line.substring(equalsIdx+1, line.length()); } else { key = line; value = null; } logger.debug("key="+key+",val="+value); if (mode == ReadState.READ_DS) { // passwords are special, because the spectacular obfustaction technique // can create bytes that are not in the range 32-127, which causes Java to // map them to chars whose numeric value isn't the byte value! // So, we have to read the "encrypted" password as an array of bytes. if (key.equals("PWD") && value != null) { byte[] cypherBytes = new byte[lineBytes.length - equalsIdx - 1]; System.arraycopy(lineBytes, equalsIdx + 1, cypherBytes, 0, cypherBytes.length); value = decryptPassword(9, cypherBytes); } currentDS.put(key, value); } else if (mode == ReadState.READ_TYPE) { if (key.matches("ca.sqlpower.sqlobject.SQLTypePhysicalProperties_\\d+")) { String[] values = value.split(","); String typeUUID = values[0]; SQLTypePhysicalProperties props = new SQLTypePhysicalProperties(currentType.getName()); props.setName(values[1]); props.setPrecisionType(PropertyType.valueOf(values[2])); props.setScaleType(PropertyType.valueOf(values[3])); typePropertiesMap.put(typeUUID, props); } else { currentType.putProperty(key, value); } } else if (mode == ReadState.READ_GENERIC) { currentSection.put(key, value); } else if (mode == ReadState.READ_SQLTYPE) { putPropertyIntoSQLType(currentSQLType, key, value); } } } in.close(); mergeFileData(mode, currentType, currentDS, currentSQLType, currentSection); // hook up database type hierarchy, and assign parentType pointers to data sources themselves for (Object o : fileSections) { if (o instanceof JDBCDataSourceType) { JDBCDataSourceType dst = (JDBCDataSourceType) o; String parentTypeName = dst.getProperty(JDBCDataSourceType.PARENT_TYPE_NAME); if (parentTypeName != null) { JDBCDataSourceType parentType = getDataSourceType(parentTypeName); if (parentType == null) { throw new IllegalStateException( "Database type \""+dst.getName()+"\" refers to parent type \""+ parentTypeName+"\", which doesn't exist"); } dst.setParentType(parentType); } } else if (o instanceof JDBCDataSource) { JDBCDataSource ds = (JDBCDataSource) o; String typeName = ds.getPropertiesMap().get(JDBCDataSource.DBCS_CONNECTION_TYPE); if (typeName != null) { JDBCDataSourceType type = getDataSourceType(typeName); if (type == null) { logger.error( "Database connection \""+ds.getName()+"\" refers to database type \""+ typeName+"\", which doesn't exist"); } else { logger.debug("The data source type \"" + type + "\" is being set as the parent type of" + ds); ds.setParentType(type); } } } else if (o instanceof UserDefinedSQLType) { // Attach SQLTypePhysicalProperties to their type UserDefinedSQLType type = (UserDefinedSQLType) o; Collection<SQLTypePhysicalProperties> typeProperties = typePropertiesMap.get(type.getUUID()); if (typeProperties != null) { for (SQLTypePhysicalProperties properties : typeProperties) { type.putPhysicalProperties(properties.getPlatform(), properties); } } } } } finally { dontAutoSave = false; } logger.info("Finished reading file."); } /** * This method exists to convert older properties in a Pl.ini file to new properties. * This is for cases where a class was moved causing its full class name to be modified. * @param line * A line of text in the Pl.ini file that may need to be converted. * @return * The same line of text passed in with possible parts of it modified. */ private String convertOldLines(String line) { if (line.contains("ca.sqlpower.architect.SQLIndex")) return line.replace("ca.sqlpower.architect.SQLIndex", SQLIndex.class.getName()); return line; } /** * A subroutine of the read() method. Merges data from any of the three types of * sections into the fileSections collection. * <p> * A better approach than this would be to have SPDataSourceType, SPDataSource, * and Section all implement some interface, and then just have one merge() method. * * @param mode File parsing mode. Determines which mergeXXX() method is called, and * which argument is passed in. * @param currentType Only used when mode = READ_TYPE * @param currentDS Only used when mode = READ_DS * @param currentSQLType Only used when mode = READ_SQLTYPE * @param currentSection Only used when mode = READ_GENERIC */ private void mergeFileData(ReadState mode, JDBCDataSourceType currentType, SPDataSource currentDS, UserDefinedSQLType currentSQLType, Section currentSection) { if (mode == ReadState.READ_DS) { mergeDataSource(currentDS); } else if (mode == ReadState.READ_GENERIC) { mergeSection(currentSection); } else if (mode == ReadState.READ_SQLTYPE) { mergeSQLType(currentSQLType); } else if (mode == ReadState.READ_TYPE) { // special case: sometimes the parser ends up thinking there was // an empty ds type section at the end of the file. we can't merge it. if (currentType.getProperties().size() > 0) { mergeDataSourceType(currentType); } } else { throw new IllegalArgumentException("Unknown read state. Can't merge"); } } private void mergeSection(Section currentSection) { logger.debug("Attempting to merge Section: \"" + currentSection.getName() + "\""); for (Object o : fileSections) { if (o instanceof Section) { Section s = (Section) o; if ( (s.getName() == null && currentSection.getName() == null) || (s.getName() != null && s.getName().equals(currentSection.getName())) ) { logger.debug("Found a section to merge, now merging"); s.merge(currentSection); return; } } } logger.debug("Didn't find section to merge. Adding..."); fileSections.add(currentSection); } /** * Reads bytes from the input stream until a CRLF pair or end-of-file is encountered. * If a line is longer than some arbitrary maximum (currently 10000 bytes), it will * be split into pieces not larger than that size and returned as separate lines. * In this case, an error will be logged to the class's logger. * * <p>Note: We think that we require CRLF line ends because the encrypted password * could contain a bare CR or LF, which we don't want to interpret as an end-of-line. * * @param in The input stream to read. * @return All of the bytes read except the terminating CRLF. If there are no more * bytes to read (because in is already at end-of-file) then this method returns null. */ private byte[] readLine(BufferedInputStream in) throws IOException { final int MAX_LINE_LENGTH = 10000; byte[] buffer = new byte[MAX_LINE_LENGTH]; int lineSize = 0; int ch; while ( (ch = in.read()) != -1 && lineSize < MAX_LINE_LENGTH) { buffer[lineSize] = (byte) ch; lineSize++; if (lineSize >= 2 && buffer[lineSize-2] == '\r' && buffer[lineSize-1] == '\n') { lineSize -= 2; break; } } // check for end of file if (ch == -1 && lineSize == 0) return null; // check for split lines if (lineSize == MAX_LINE_LENGTH) logger.error("Maximum line size exceeded while reading pl.ini. Line will be split up."); byte chopBuffer[] = new byte[lineSize]; System.arraycopy(buffer, 0, chopBuffer, 0, lineSize); return chopBuffer; } public void write() throws IOException { if (lastFileAccessed == null) { throw new IllegalStateException("Can't determine location for saving"); } write(lastFileAccessed); } /* (non-Javadoc) * @see ca.sqlpower.architect.DataSourceCollection#write(java.io.File) */ public void write(File location) throws IOException { logger.debug("Writing to "+location); try { dontAutoSave = true; OutputStream out = new BufferedOutputStream(new FileOutputStream(location)); write(out); out.close(); lastFileAccessed = location; fileTime = location.lastModified(); } finally { dontAutoSave = false; } } /** * Writes the data source types and the data sources of this instance * in the world-famous PL.INI format. Doesn't affect the lastFileLocation * or anything. */ public void write(OutputStream out) throws IOException { // counting starts at 1. Yay, VB! int dbNum = 1; int typeNum = 1; int olapNum = 1; int sqltypeNum = 1; Iterator<Object> it = fileSections.iterator(); while (it.hasNext()) { Object next = it.next(); if (next instanceof Section) { writeSection(out, ((Section) next).getName(), ((Section) next).getPropertiesMap()); } else if (next instanceof JDBCDataSource) { writeSection(out, "Databases_"+dbNum, ((JDBCDataSource) next).getPropertiesMap()); dbNum++; } else if (next instanceof JDBCDataSourceType) { List<SQLTypePhysicalProperties> properties = getPropertiesForDataSourceType((JDBCDataSourceType) next); Map<String, String> propMap = new LinkedHashMap<String,String>(((JDBCDataSourceType) next).getProperties()); for (int i = 0 ; i < properties.size() ; i++) { SQLTypePhysicalProperties typeProp = properties.get(i); StringBuilder value = new StringBuilder(); value.append(typeProp.getParent().getUUID()).append(","); value.append(typeProp.getName()).append(","); value.append(typeProp.getPrecisionType()).append(","); value.append(typeProp.getScaleType()); propMap.put("ca.sqlpower.sqlobject.SQLTypePhysicalProperties_" + i, value.toString()); } writeSection(out, "Database Types_"+typeNum, propMap); typeNum++; } else if (next instanceof Olap4jDataSource) { writeSection(out, "OLAP_databases_"+olapNum, ((Olap4jDataSource) next).getPropertiesMap()); olapNum++; } else if (next instanceof UserDefinedSQLType) { Map<String, String> typeProperties = getPropertiesFromSQLType((UserDefinedSQLType) next); writeSection(out, "Data Types_"+sqltypeNum, typeProperties); sqltypeNum++; } else if (next == null) { logger.error("write: Null section"); } else { logger.error("write: Unknown section type: "+next.getClass().getName()); } } } private List<SQLTypePhysicalProperties> getPropertiesForDataSourceType(JDBCDataSourceType ds) { List<SQLTypePhysicalProperties> propertiesList = new ArrayList<SQLTypePhysicalProperties>(); for (Object o : fileSections) { if (o instanceof UserDefinedSQLType) { UserDefinedSQLType type = (UserDefinedSQLType) o; SQLTypePhysicalProperties properties = type.getPhysicalProperties(ds.getName()); if (properties != null && properties.getPlatform().equals(ds.getName())) { propertiesList.add(properties); } } } return propertiesList; } /** * A helper method that given a {@link UserDefinedSQLType}, it returns an * unmodifiable map of properties that are compatible with the expected key * values for the Data Types section in the PL.INI */ // Left this package private so that the PLDotIniTest can use it Map<String,String> createSQLTypePropertiesMap(UserDefinedSQLType next) { Map<String, String> properties = new LinkedHashMap<String, String>(); properties.put("Name", next.getName()); properties.put("Basic Type", next.getBasicType().toString()); properties.put("Description", next.getDescription()); properties.put("JDBC Type", String.valueOf(next.getType())); return Collections.unmodifiableMap(properties); } /** * A helper method that takes a value/key pair associate with a new Data * Type and sets them on the given {@link UserDefinedSQLType}. */ private void putPropertyIntoSQLType(UserDefinedSQLType sqlType, String key, String value) { String platform = SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM; if (key.equals("Name")) { sqlType.setName(value); sqlType.setPhysicalTypeName(platform, value); } else if (key.equals("Basic Type")) { sqlType.setBasicType(BasicSQLType.valueOf(value)); } else if (key.equals("Description")) { sqlType.setMyDescription(value); } else if (key.equals("JDBC Type")) { sqlType.setType(Integer.valueOf(value)); } else if (key.equals("Precision Type")) { sqlType.setPrecisionType(platform, PropertyType.valueOf(value)); } else if (key.equals("Scale Type")) { sqlType.setScaleType(platform, PropertyType.valueOf(value)); } else if (key.equals("UUID")) { sqlType.setUUID(value); } } private Map<String,String> getPropertiesFromSQLType(UserDefinedSQLType sqlType) { String platform = SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM; Map<String, String> typeProperties = new LinkedHashMap<String,String>(); typeProperties.put("Name", sqlType.getName()); typeProperties.put("UUID", sqlType.getUUID()); typeProperties.put("Basic Type", sqlType.getBasicType().toString()); typeProperties.put("Description", sqlType.getDescription()); typeProperties.put("Precision Type", sqlType.getPrecisionType(platform).toString()); typeProperties.put("Scale Type", sqlType.getScaleType(platform).toString()); typeProperties.put("JDBC Type", sqlType.getType().toString()); return typeProperties; } /** * Writes out the named section header, followed by all the properties, one per line. Each * line is terminated with a CRLF, regardless of the current platform default. * * @param out The output stream to write to. * @param name The name of the section. * @param properties The properties to output in this section. * @throws IOException when writing to the given stream fails. */ private void writeSection(OutputStream out, String name, Map<String, String> properties) throws IOException { if (name != null) { String sectionHeading = "["+name+"]" + DOS_CR_LF; out.write(sectionHeading.getBytes()); } // output LOGICAL first (if it exists) String s = null; if ((s = (String) properties.get("Logical")) != null) { out.write("Logical".getBytes()); out.write("=".getBytes()); out.write(s.getBytes()); out.write(DOS_CR_LF.getBytes()); } // now get everything else, and ignore the LOGICAL property Iterator<Map.Entry<String, String>> it = properties.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, String> ent = it.next(); if (!ent.getKey().equals("Logical")) { out.write(((String) ent.getKey()).getBytes()); if (ent.getValue() != null) { byte[] val; if (ent.getKey().equals("PWD")) { val = encryptPassword(9, ((String) ent.getValue())); } else { val = ((String) ent.getValue()).getBytes(); } out.write("=".getBytes()); out.write(val); } out.write(DOS_CR_LF.getBytes()); } } } public SPDataSource getDataSource(String name) { return getDataSource(name, SPDataSource.class); } public <C extends SPDataSource> C getDataSource(String name, Class<C> classType) { Iterator<Object> it = fileSections.iterator(); while (it.hasNext()) { Object next = it.next(); if (classType.isInstance(next)) { C ds = classType.cast(next); if (logger.isDebugEnabled()) { logger.debug("Checking if data source "+ds+" is PL Logical connection "+name); } if (ds.getName().equals(name)) return ds; } } return null; } public JDBCDataSourceType getDataSourceType(String name) { for (Object next : fileSections) { if (next instanceof JDBCDataSourceType) { JDBCDataSourceType dst = (JDBCDataSourceType) next; if (logger.isDebugEnabled()) { logger.debug("Checking if data source type "+dst+" is called "+name); } if (dst.getName().equals(name)) return dst; } } return null; } public List<JDBCDataSourceType> getDataSourceTypes() { List<JDBCDataSourceType> list = new ArrayList<JDBCDataSourceType>(); for (Object next : fileSections) { if (next instanceof JDBCDataSourceType) { JDBCDataSourceType dst = (JDBCDataSourceType) next; list.add(dst); } } return list; } /* Creates a list of data sources by iterating over all the sections and * picking the ones that are SPDataSource items. Yes, this is not * optimal, but we can defer optimising it until someone proves it's an * actual performance issue. */ public List<SPDataSource> getConnections() { return getConnections(SPDataSource.class); } /** * Gets a list of SPDataSource objects that are sorted. This will get only * {@link SPDataSource} objects of the specified type. * @param classType * @return */ public <C extends SPDataSource> List<C> getConnections(Class<C> classType) { List<C> connections = new ArrayList<C>(); Iterator<Object> it = fileSections.iterator(); while (it.hasNext()) { Object next = it.next(); if (classType.isInstance(next)) { connections.add(classType.cast(next)); } } Collections.sort(connections); return connections; } /* (non-Javadoc) * @see ca.sqlpower.architect.DataSourceCollection#toString() */ public String toString() { OutputStream out = new ByteArrayOutputStream(); try { write(out); } catch (IOException e) { return "PlDotIni: toString: Couldn't create string description: "+e.getMessage(); } return out.toString(); } /** * Encrypts a PL.INI password. The correct argument for * <code>key</code> is 9. */ private byte[] encryptPassword(int key, String plaintext) { byte[] cyphertext = new byte[plaintext.length()]; int temp; for (int i = 0; i < plaintext.length(); i++) { temp = plaintext.charAt(i); if (i % 2 == 1) { // if odd (even in VB's 1-based indexing) temp = temp - key; } else { temp = temp + key; } temp = temp ^ (10 - key); cyphertext[i] = ((byte) temp); } if (logger.isDebugEnabled()) { StringBuffer nums = new StringBuffer(); for (int i = 0; i < cyphertext.length; i++) { nums.append((int) cyphertext[i]); nums.append(' '); } logger.debug("Encrypt: Plaintext: \""+plaintext+"\"; cyphertext=("+nums+")"); } return cyphertext; } /** * Decrypts a PL.INI password. The correct argument for * <code>number</code> is 9. */ public static String decryptPassword(int number, byte[] cyphertext) { StringBuffer plaintext = new StringBuffer(cyphertext.length); for (int i = 0, n = cyphertext.length; i < n; i++) { int temp = (( ((int) cyphertext[i]) & 0x00ff) ^ (10 - number)); if (i % 2 == 1) { temp += number; } else { temp -= number; } plaintext.append((char) temp); } if (logger.isDebugEnabled()) { StringBuffer nums = new StringBuffer(); for (int i = 0; i < cyphertext.length; i++) { nums.append((int) cyphertext[i]); nums.append(' '); } logger.debug("Decrypt: cyphertext=("+nums+"); Plaintext: \""+plaintext+"\""); } return plaintext.toString(); } /* (non-Javadoc) * @see ca.sqlpower.architect.DataSourceCollection#addDataSource(ca.sqlpower.architect.SPDataSource) */ public void addDataSource(SPDataSource dbcs) { String newName = dbcs.getDisplayName(); for (Object o : fileSections) { if (o instanceof SPDataSource) { SPDataSource oneDbcs = (SPDataSource) o; if (newName.equalsIgnoreCase(oneDbcs.getDisplayName())) { throw new IllegalArgumentException( "There is already a datasource with the name " + newName); } } } addDataSourceImpl(dbcs); } /* (non-Javadoc) * @see ca.sqlpower.architect.DataSourceCollection#mergeDataSource(ca.sqlpower.architect.SPDataSource) */ public void mergeDataSource(SPDataSource dbcs) { String newName = dbcs.getDisplayName(); for (Object o : fileSections) { if (o instanceof SPDataSource) { SPDataSource oneDbcs = (SPDataSource) o; if (newName.equalsIgnoreCase(oneDbcs.getDisplayName())) { for (Map.Entry<String, String> ent : dbcs.getPropertiesMap().entrySet()) { oneDbcs.put(ent.getKey(), ent.getValue()); } return; } } } // we only get here if we didn't find a data source to update. addDataSourceImpl(dbcs); } /* (non-Javadoc) * @see ca.sqlpower.architect.DataSourceCollection#removeDataSource(ca.sqlpower.architect.SPDataSource) */ public void removeDataSource(SPDataSource dbcs) { // need to know the index we're removing in order to fire the remove event // (so using an indexed for loop, not a ListIterator) for ( int where=0; where<fileSections.size(); where++ ) { Object o = fileSections.get(where); if (o instanceof SPDataSource) { SPDataSource current = (SPDataSource) o; if (current.getName().equals(dbcs.getName())) { fileSections.remove(where); fireRemoveEvent(where, dbcs); return; } } } throw new IllegalArgumentException("dbcs not in list"); } /** * Copies all the properties in the given dst into the existing DataSourceType section * with the same name, if one exists. Otherwise, adds the given dst as a new section. */ public void mergeDataSourceType(JDBCDataSourceType dst) { logger.debug("Merging data source type "+dst.getName()); String newName = dst.getName(); if (newName == null) { throw new IllegalArgumentException("Can't merge a nameless data source type: "+dst); } for (Object o : fileSections) { if (o instanceof JDBCDataSourceType) { JDBCDataSourceType current = (JDBCDataSourceType) o; if (newName.equalsIgnoreCase(current.getName())) { logger.debug(" Found it"); for (Map.Entry<String, String> ent : dst.getProperties().entrySet()) { current.putProperty(ent.getKey(), ent.getValue()); } return; } } } logger.debug(" Not found.. adding"); addDataSourceType(dst); } /** * Common code for add and merge. Adds the given dbcs as a section, then fires an add event. * @param dbcs */ private void addDataSourceImpl(SPDataSource dbcs) { fileSections.add(dbcs); fireAddEvent(dbcs); } public void addDataSourceType(JDBCDataSourceType dataSourceType) { // TODO fire an event for adding the dstype fileSections.add(dataSourceType); for (int i = undoListeners.size() - 1; i >= 0; i--) { undoListeners.get(i).undoableEditHappened(new UndoableEditEvent(this, new AddDSTypeUndoableEdit(dataSourceType))); } } public boolean removeDataSourceType(JDBCDataSourceType dataSourceType) { // TODO fire an event for removing the dstype for (int i = undoListeners.size() - 1; i >= 0; i--) { undoListeners.get(i).undoableEditHappened(new UndoableEditEvent(this, new RemoveDSTypeUndoableEdit(dataSourceType))); } return fileSections.remove(dataSourceType); } /** * If a section with the same name as the given sqlType exists, then merge * the properties. Otherwise, add a new section for this type. */ public void mergeSQLType(UserDefinedSQLType sqlType) { String name = sqlType.getName(); for (Object o : fileSections) { if (o instanceof UserDefinedSQLType) { UserDefinedSQLType existingType = (UserDefinedSQLType) o; if (existingType.getName().equals(name)) { for (Map.Entry<String, String> entry : createSQLTypePropertiesMap(sqlType).entrySet()) { putPropertyIntoSQLType(existingType, entry.getKey(), entry.getValue()); } return; } } } addSQLType(sqlType); } private void addSQLType(UserDefinedSQLType sqlType) { // TODO: If this method is made public for client code to add new types, // there will probably have to be events fired for UI fileSections.add(sqlType); } private void fireAddEvent(SPDataSource dbcs) { int index = fileSections.size()-1; DatabaseListChangeEvent e = new DatabaseListChangeEvent(this, index, dbcs); synchronized(listeners) { for(DatabaseListChangeListener listener : listeners) { listener.databaseAdded(e); } } } private void fireRemoveEvent(int i, SPDataSource dbcs) { DatabaseListChangeEvent e = new DatabaseListChangeEvent(this, i, dbcs); synchronized(listeners) { for(DatabaseListChangeListener listener : listeners) { listener.databaseRemoved(e); } } } /* (non-Javadoc) * @see ca.sqlpower.architect.DataSourceCollection#addDatabaseListChangeListener(ca.sqlpower.architect.DatabaseListChangeListener) */ public void addDatabaseListChangeListener(DatabaseListChangeListener l) { synchronized(listeners) { listeners.add(l); } } /* (non-Javadoc) * @see ca.sqlpower.architect.DataSourceCollection#removeDatabaseListChangeListener(ca.sqlpower.architect.DatabaseListChangeListener) */ public void removeDatabaseListChangeListener(DatabaseListChangeListener l) { synchronized(listeners) { listeners.remove(l); } } public void addUndoableEditListener(UndoableEditListener l) { undoListeners.add(l); } public void removeUndoableEditListener(UndoableEditListener l) { undoListeners.remove(l); } public URI getServerBaseURI() { return serverBaseURI; } public URI getMondrianServerBaseURI() { return mondrianServerBaseURI; } public UserDefinedSQLType getSQLType(String name) { for (Object o : fileSections) { if (o instanceof UserDefinedSQLType) { if (((UserDefinedSQLType) o).getName().equals(name)) { return (UserDefinedSQLType) o; } } } return null; } public List<UserDefinedSQLType> getSQLTypes() { List<UserDefinedSQLType> list = new ArrayList<UserDefinedSQLType>(); for (Object o : fileSections) { if (o instanceof UserDefinedSQLType) { list.add((UserDefinedSQLType) o); } } return list; } /** * This behaviour is not supported in the standalone edition, and so returns a stub type */ public UserDefinedSQLType getNewSQLType(String name, int jdbcCode) { for (UserDefinedSQLType type : getSQLTypes()) { if (type.getUUID().equals(unknownTypeUUID)) return type; } throw new IllegalStateException("The Unknown Datatype has been removed"); } }