Though it's perhaps not exactly as I'd prefer, a consensus has formed on where

to put your source and test source code for Java projects. I'm going to toe the
line here because I want to use SBT to publish samskivert to the centralized
Maven repositories.


git-svn-id: https://samskivert.googlecode.com/svn/trunk@2807 6335cc39-0255-0410-8fd6-9bcaacd3b74c
This commit is contained in:
samskivert
2010-08-27 00:08:06 +00:00
parent 3dcef4a00c
commit b03237ee4d
314 changed files with 60 additions and 141 deletions
+32
View File
@@ -0,0 +1,32 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert;
import com.samskivert.util.Logger;
/**
* Contains a reference to the log object used by the samskivert package.
*/
public class Log
{
/** We dispatch our log messages through this logger. */
public static final Logger log = Logger.getLogger("com.samskivert");
}
@@ -0,0 +1,6 @@
<!-- $Id$ -->
<!-- Exports various samskivert utilities to GWT. -->
<module>
<source path="text"/>
<source path="util"/>
</module>
@@ -0,0 +1,50 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* An annotation for indicating that something has a preferred implementation elsewhere, without
* going so far as to deprecate it.
*/
@Documented
@Inherited
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface ReplacedBy
{
/**
* A human-readable String containing a reference to the replacement Class, method, or field.
* It is suggested that you follow the "@see" javadoc semantics for specifying a reference.
*/
String value ();
/**
* The reason the replacement is suggested, in case it's not obvious.
*/
String reason() default "";
}
@@ -0,0 +1,65 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.io;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
/**
* The byte array out/input stream is used for writing data to a byte
* array output stream and then obtaining an input stream that can read
* back the data written to the output stream without first having to copy
* it to a separate buffer.
*/
public class ByteArrayOutInputStream extends ByteArrayOutputStream
{
/**
* Creates a new byte array out/input stream. The buffer capacity is
* initially 32 bytes, though its size increases if necessary.
*/
public ByteArrayOutInputStream ()
{
this(32);
}
/**
* Creates a new byte array out/input stream, with a buffer capacity
* of the specified size, in bytes.
*
* @param size the initial size.
*
* @exception IllegalArgumentException if size is negative.
*/
public ByteArrayOutInputStream (int size)
{
super(size);
}
/**
* Returns an input stream configured to read only the bytes that have
* been written this far to this byte array out/input stream.
*/
public InputStream getInputStream ()
{
return new ByteArrayInputStream(buf, 0, count);
}
}
@@ -0,0 +1,205 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.io;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
/**
* Wraps a supplied {@link PrintStream} to allow capturing all data
* written to the stream via the various <code>print()</code> and
* <code>println()</code> variants. Derived classes must implement the
* {@link #handlePrinted} and {@link #handleNewLine} methods.
*/
public abstract class ExtensiblePrintStream extends PrintStream
{
/**
* Constructs an extensible print stream.
*/
public ExtensiblePrintStream (PrintStream out)
{
super(out);
}
/**
* Constructs an extensible print stream.
*/
public ExtensiblePrintStream (PrintStream out, boolean autoFlush)
{
super(out, autoFlush);
}
/**
* Constructs an extensible print stream.
*/
public ExtensiblePrintStream (
PrintStream out, boolean autoFlush, String encoding)
throws UnsupportedEncodingException
{
super(out, autoFlush, encoding);
}
/**
* Called with any text printed to the stream, excepting newlines
* resulting from calls to the various <code>println()</code> methods,
* which are reported via {@link #handleNewLine}.
*/
public abstract void handlePrinted (String s);
/**
* Called whenever a newline is printed to the stream by one of the
* various <code>println()</code> methods.
*/
public abstract void handleNewLine ();
@Override
public void print (boolean b)
{
super.print(b);
handlePrinted(b ? "true" : "false");
}
@Override
public void print (char c)
{
super.print(c);
handlePrinted(String.valueOf(c));
}
@Override
public void print (int i)
{
super.print(i);
handlePrinted(String.valueOf(i));
}
@Override
public void print (long l)
{
super.print(l);
handlePrinted(String.valueOf(l));
}
@Override
public void print (float f)
{
super.print(f);
handlePrinted(String.valueOf(f));
}
@Override
public void print (double d)
{
super.print(d);
handlePrinted(String.valueOf(d));
}
@Override
public void print (char[] s)
{
super.print(s);
handlePrinted(String.valueOf(s));
}
@Override
public void print (String s)
{
super.print(s);
handlePrinted((s == null) ? "null" : s);
}
@Override
public void print (Object obj)
{
super.print(obj);
handlePrinted(String.valueOf(obj));
}
@Override
public void println ()
{
super.println();
handleNewLine();
}
@Override
public void println (boolean x)
{
super.println(x);
handleNewLine();
}
@Override
public void println (char x)
{
super.println(x);
handleNewLine();
}
@Override
public void println (int x)
{
super.println(x);
handleNewLine();
}
@Override
public void println (long x)
{
super.println(x);
handleNewLine();
}
@Override
public void println (float x)
{
super.println(x);
handleNewLine();
}
@Override
public void println (double x)
{
super.println(x);
handleNewLine();
}
@Override
public void println (char[] x)
{
super.println(x);
handleNewLine();
}
@Override
public void println (String x)
{
super.println(x);
handleNewLine();
}
@Override
public void println (Object x)
{
super.println(x);
handleNewLine();
}
}
@@ -0,0 +1,59 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.io;
/**
* A persistence exception can be thrown when an error occurs in
* underlying persistence code. By encapsulating errors, one retains the
* ability to make changes to the implementation structure without
* affecting the interface to persistence services presented to the
* application.
*/
public class PersistenceException extends Exception
{
/**
* Constructs a persistence exception with the specified error
* message.
*/
public PersistenceException (String message)
{
super(message);
}
/**
* Constructs a persistence exception with the specified error message
* and the chained causing event.
*/
public PersistenceException (String message, Exception cause)
{
super(message);
initCause(cause);
}
/**
* Constructs a persistence exception with the specified chained
* causing event.
*/
public PersistenceException (Exception cause)
{
initCause(cause);
}
}
@@ -0,0 +1,147 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.io;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import static com.samskivert.Log.log;
/**
* Convenience methods for streams.
*/
public class StreamUtil
{
/**
* Convenient close for a stream. Use in a finally clause and love life.
*/
public static void close (InputStream in)
{
if (in != null) {
try {
in.close();
} catch (IOException ioe) {
log.warning("Error closing input stream", "stream", in, "cause", ioe);
}
}
}
/**
* Convenient close for a stream. Use in a finally clause and love life.
*/
public static void close (OutputStream out)
{
if (out != null) {
try {
out.close();
} catch (IOException ioe) {
log.warning("Error closing output stream", "stream", out, "cause", ioe);
}
}
}
/**
* Convenient close for a Reader. Use in a finally clause and love life.
*/
public static void close (Reader in)
{
if (in != null) {
try {
in.close();
} catch (IOException ioe) {
log.warning("Error closing reader", "reader", in, "cause", ioe);
}
}
}
/**
* Convenient close for a Writer. Use in a finally clause and love life.
*/
public static void close (Writer out)
{
if (out != null) {
try {
out.close();
} catch (IOException ioe) {
log.warning("Error closing writer", "writer", out, "cause", ioe);
}
}
}
/**
* Copies the contents of the supplied input stream to the supplied output stream.
*/
public static <T extends OutputStream> T copy (InputStream in, T out)
throws IOException
{
byte[] buffer = new byte[4096];
for (int read = 0; (read = in.read(buffer)) > 0; ) {
out.write(buffer, 0, read);
}
return out;
}
/**
* Reads the contents of the supplied stream into a byte array.
*/
public static byte[] toByteArray (InputStream stream)
throws IOException
{
return copy(stream, new ByteArrayOutputStream()).toByteArray();
}
/**
* Reads the contents of the supplied stream into a string using the platform default charset.
*/
public static String toString (InputStream stream)
throws IOException
{
return copy(stream, new ByteArrayOutputStream()).toString();
}
/**
* Reads the contents of the supplied stream into a string using the supplied {@link Charset}.
*/
public static String toString (InputStream stream, String charset)
throws IOException
{
return copy(stream, new ByteArrayOutputStream()).toString(charset);
}
/**
* Reads the contents of the supplied reader into a string.
*/
public static String toString (Reader reader)
throws IOException
{
char[] inbuf = new char[4096];
StringBuilder outbuf = new StringBuilder();
for (int read = 0; (read = reader.read(inbuf)) > 0; ) {
outbuf.append(inbuf, 0, read);
}
return outbuf.toString();
}
}
@@ -0,0 +1,32 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
$Id: package.html,v 1.1 2001/08/12 01:34:31 mdb Exp $
samskivert library - useful routines for java programs
Copyright (C) 2001-2005 Michael Bayne
This library 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 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-->
</head>
<body bgcolor="white">
Various I/O related utilities.
</body>
</html>
@@ -0,0 +1,68 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import com.samskivert.io.PersistenceException;
/**
* An {@link Repository.Operation} wrapper that automatically closes all statements opened by the
* operation.
*
* @author Charlie Groves
*/
public abstract class AutoCloseOperation<V>
implements Repository.Operation<V>
{
/**
* See {@link Repository.Operation#invoke}. Any Statement or PreparedStatement objects created
* during the invocation of this method will automatically be closed.
*/
public abstract V cleanInvoke (Connection conn, DatabaseLiaison liason)
throws SQLException, PersistenceException;
/**
* Invokes {@link #cleanInvoke} and then closes any statements opened by that call.
*/
public final V invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
List<Statement> stmts = new ArrayList<Statement>(1);
conn = JDBCUtil.makeCollector(conn, stmts);
try {
return cleanInvoke(conn, liaison);
} finally {
// if closing a statement throws an SQLException, we don't attempt to close the rest of
// the statements -- an SQLException thrown on close is a problem with the underlying
// connection and the SimpleRepository closes the connection when it gets an exception
// like that; the resources for the remaining statements will be collected in that case
for (Statement stmt : stmts) {
JDBCUtil.close(stmt);
}
}
}
}
@@ -0,0 +1,321 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2006-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import com.samskivert.util.StringUtil;
import static com.samskivert.Log.log;
/**
* A superclass to help with the shrinking subset of SQL our supported dialects can agree on,
* or when there is disagreement, implement the most standard-compliant version and let the
* dialectal sub-classes override.
*/
public abstract class BaseLiaison implements DatabaseLiaison
{
public BaseLiaison ()
{
super();
}
// from DatabaseLiaison
public boolean tableExists (Connection conn, String name) throws SQLException
{
ResultSet rs = conn.getMetaData().getTables(null, null, name, null);
while (rs.next()) {
String tname = rs.getString("TABLE_NAME");
if (name.equals(tname)) {
return true;
}
}
return false;
}
// from DatabaseLiaison
public boolean tableContainsColumn (Connection conn, String table, String column)
throws SQLException
{
ResultSet rs = conn.getMetaData().getColumns(null, null, table, column);
while (rs.next()) {
String tname = rs.getString("TABLE_NAME");
String cname = rs.getString("COLUMN_NAME");
if (tname.equals(table) && cname.equals(column)) {
return true;
}
}
return false;
}
// from DatabaseLiaison
public boolean tableContainsIndex (Connection conn, String table, String index)
throws SQLException
{
ResultSet rs = conn.getMetaData().getIndexInfo(null, null, table, false, true);
while (rs.next()) {
String tname = rs.getString("TABLE_NAME");
String iname = rs.getString("INDEX_NAME");
if (tname.equals(table) && index.equals(iname)) {
return true;
}
}
return false;
}
// from DatabaseLiaison
public boolean addIndexToTable (
Connection conn, String table, String[] columns, String ixName, boolean unique)
throws SQLException
{
if (tableContainsIndex(conn, table, ixName)) {
return false;
}
ixName = (ixName != null ? ixName : StringUtil.join(columns, "_"));
StringBuilder update = new StringBuilder("CREATE ");
if (unique) {
update.append("UNIQUE ");
}
update.append("INDEX ").append(indexSQL(ixName)).append(" ON ").
append(tableSQL(table)).append(" (");
for (int ii = 0; ii < columns.length; ii ++) {
if (ii > 0) {
update.append(", ");
}
update.append(columnSQL(columns[ii]));
}
update.append(")");
executeQuery(conn, update.toString());
log.info("Database index '" + ixName + "' added to table '" + table + "'");
return true;
}
// from DatabaseLiaison
public void addPrimaryKey (Connection conn, String table, String[] columns)
throws SQLException
{
StringBuilder fields = new StringBuilder("(");
for (int ii = 0; ii < columns.length; ii ++) {
if (ii > 0) {
fields.append(", ");
}
fields.append(columnSQL(columns[ii]));
}
fields.append(")");
String update = "ALTER TABLE " + tableSQL(table) + " ADD PRIMARY KEY " + fields.toString();
executeQuery(conn, update);
log.info("Primary key " + fields + " added to table '" + table + "'");
}
// from DatabaseLiaison
public void dropIndex (Connection conn, String table, String index) throws SQLException
{
executeQuery(conn, "DROP INDEX " + columnSQL(index));
}
// from DatabaseLiaison
public void dropPrimaryKey (Connection conn, String table, String pkName)
throws SQLException
{
executeQuery(conn, "ALTER TABLE " + tableSQL(table) +
" DROP CONSTRAINT " + columnSQL(pkName));
}
// from DatabaseLiaison
public boolean addColumn (
Connection conn, String table, String column, String definition, boolean check)
throws SQLException
{
if (check && tableContainsColumn(conn, table, column)) {
return false;
}
executeQuery(conn, "ALTER TABLE " + tableSQL(table) + " ADD COLUMN " +
columnSQL(column) + " " + definition);
log.info("Database column '" + column + "' added to table '" + table + "'.");
return true;
}
// from DatabaseLiaison
public boolean addColumn (Connection conn, String table, String column,
ColumnDefinition newColumnDef, boolean check)
throws SQLException
{
if (check && tableContainsColumn(conn, table, column)) {
return false;
}
executeQuery(conn, "ALTER TABLE " + tableSQL(table) + " ADD COLUMN " +
columnSQL(column) + " " + expandDefinition(newColumnDef));
log.info("Database column '" + column + "' added to table '" + table + "'.");
return true;
}
// from DatabaseLiaison
public boolean changeColumn (Connection conn, String table, String column, String type,
Boolean nullable, Boolean unique, String defaultValue)
throws SQLException
{
String defStr = expandDefinition(
type, nullable != null ? nullable : false, unique != null ? unique : false,
defaultValue);
executeQuery(conn, "ALTER TABLE " + tableSQL(table) + " CHANGE " +
columnSQL(column) + " " + columnSQL(column) + " " + defStr);
log.info("Database column '" + column + "' of table '" + table + "' modified to have " +
"definition '" + defStr + "'.");
return true;
}
// from DatabaseLiaison
public boolean renameColumn (Connection conn, String table, String from, String to,
ColumnDefinition newColumnDef)
throws SQLException
{
executeQuery(conn, "ALTER TABLE " + tableSQL(table) + " RENAME COLUMN " +
columnSQL(from) + " TO " + columnSQL(to));
log.info("Renamed column '" + from + "' on table '" + table + "' to '" + to + "'");
return true;
}
// from DatabaseLiaison
public boolean dropColumn (Connection conn, String table, String column) throws SQLException
{
if (!tableContainsColumn(conn, table, column)) {
return false;
}
executeQuery(conn, "ALTER TABLE " + tableSQL(table) + " DROP COLUMN " + columnSQL(column));
log.info("Database column '" + column + "' removed from table '" + table + "'.");
return true;
}
// from DatabaseLiaison
public boolean createTableIfMissing (
Connection conn, String table, String[] columns, ColumnDefinition[] definitions,
String[][] uniqueConstraintColumns, String[] primaryKeyColumns)
throws SQLException
{
if (tableExists(conn, table)) {
return false;
}
if (columns.length != definitions.length) {
throw new IllegalArgumentException("Column name and definition number mismatch");
}
StringBuilder builder =
new StringBuilder("CREATE TABLE ").append(tableSQL(table)).append(" (");
for (int ii = 0; ii < columns.length; ii ++) {
if (ii > 0) {
builder.append(", ");
}
builder.append(columnSQL(columns[ii])).append(" ");
builder.append(expandDefinition(definitions[ii]));
}
if (uniqueConstraintColumns != null && uniqueConstraintColumns.length > 0) {
for (String[] uCols : uniqueConstraintColumns) {
builder.append(", UNIQUE (");
for (int ii = 0; ii < uCols.length; ii ++) {
if (ii > 0) {
builder.append(", ");
}
builder.append(columnSQL(uCols[ii]));
}
builder.append(")");
}
}
if (primaryKeyColumns != null && primaryKeyColumns.length > 0) {
builder.append(", PRIMARY KEY (");
for (int ii = 0; ii < primaryKeyColumns.length; ii ++) {
if (ii > 0) {
builder.append(", ");
}
builder.append(columnSQL(primaryKeyColumns[ii]));
}
builder.append(")");
}
builder.append(")");
executeQuery(conn, builder.toString());
log.info("Database table '" + table + "' created.");
return true;
}
public boolean dropTable (Connection conn, String name)
throws SQLException
{
if (!tableExists(conn, name)) {
return false;
}
executeQuery(conn, "DROP TABLE " + tableSQL(name));
log.info("Table '" + name + "' dropped.");
return true;
}
/**
* Create an SQL string that summarizes a column definition in that format generally
* accepted in table creation and column addition statements, e.g.
* INTEGER UNIQUE NOT NULL DEFAULT 100
*/
public String expandDefinition (ColumnDefinition def)
{
return expandDefinition(def.type, def.nullable, def.unique, def.defaultValue);
}
protected int executeQuery (Connection conn, String query)
throws SQLException
{
Statement stmt = conn.createStatement();
try {
return stmt.executeUpdate(query);
} finally {
JDBCUtil.close(stmt);
}
}
protected String expandDefinition (
String type, boolean nullable, boolean unique, String defaultValue)
{
StringBuilder builder = new StringBuilder(type);
if (!nullable) {
builder.append(" NOT NULL");
}
if (unique) {
builder.append(" UNIQUE");
}
// append the default value if one was specified
if (defaultValue != null) {
builder.append(" DEFAULT ").append(defaultValue);
}
return builder.toString();
}
}
@@ -0,0 +1,51 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2006-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
/**
* An object representing the configuration of a typical SQL column.
*/
public class ColumnDefinition
{
public String type;
public boolean nullable;
public boolean unique;
public String defaultValue;
public ColumnDefinition ()
{
this(null);
}
public ColumnDefinition (String type)
{
this(type, false, false, null);
}
public ColumnDefinition (
String type, boolean nullable, boolean unique, String defaultValue)
{
this.type = type;
this.nullable = nullable;
this.unique = unique;
this.defaultValue = defaultValue;
}
}
@@ -0,0 +1,96 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.*;
import com.samskivert.io.PersistenceException;
/**
* As the repository aims to interface with whatever database pooling services a project cares to
* use, it obtains all of its database connections through a connection provider. The connection
* provider provides connections based on a database identifier (a string) which identifies a
* particular connection to a particular database. A user of these services would then coordinate
* the database identifier (or identifiers) used to invoke a database operation with the database
* connections that are returned by the connection provider that they provided to the repository at
* construct time.
*/
public interface ConnectionProvider
{
/**
* Obtains a database connection based on the supplied database identifier. The repository
* expects to have exclusive use of this connection instance until it releases it. This
* connection will be released subsequently with a call to {@link #releaseConnection} or {@link
* #connectionFailed} depending on the circumstances of the release. <code>close()</code>
* <em>will not</em> be called on the connection. It is up to the connection provider to close
* the connection when it is released if appropriate.
*
* @param ident the database connection identifier.
* @param readOnly whether or not the connection may be to a read-only mirror of the
* repository.
*
* @return an active JDBC connection (which may have come from a connection pool).
*
* @exception PersistenceException thrown if a problem occurs trying to open the requested
* connection.
*/
public Connection getConnection (String ident, boolean readOnly)
throws PersistenceException;
/**
* Releases a database connection when it is no longer needed by the repository.
*
* @param ident the database identifier used when obtaining this connection.
* @param readOnly the same value that was passed to {@link #getConnection} to obtain this
* connection.
* @param conn the connection to release (back into the pool or to be closed if pooling is not
* going on).
*/
public void releaseConnection (String ident, boolean readOnly, Connection conn);
/**
* Called by the repository if a failure occurred on the connection. It is expected that the
* connection will be disposed of and subsequent calls to <code>getConnection</code> will
* return a freshly established connection to the database.
*
* @param ident the database identifier used when obtaining this connection.
* @param readOnly the same value that was passed to {@link #getConnection} to obtain this
* connection.
* @param conn the connection that failed.
* @param error the error thrown by the connection (which may be used to determine if the
* connection should be closed or can be reused).
*/
public void connectionFailed (String ident, boolean readOnly, Connection conn,
SQLException error);
/**
* Returns the URL associated with this database identifier. This should be the same value that
* would be used if {@link #getConnection} were called.
*/
public String getURL (String ident);
/**
* Shuts down this connection provider, closing all connections currently in the pool. This
* should only be called once all active connections have been released with {@link
* #releaseConnection}.
*/
public void shutdown ();
}
@@ -0,0 +1,110 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import com.samskivert.io.PersistenceException;
import static com.samskivert.Log.log;
/**
* Provides connections using a pair of {@link DataSource} instances (one for read-only operations
* and one for read-write operations).
*/
public class DataSourceConnectionProvider implements ConnectionProvider
{
/**
* Creates a connection provider that will obtain connections from the supplied read-only and
* read-write sources. The <code>ident</code> mechanism is not used by this provider.
* Responsibility for the lifecycle of the supplied datasources is left to the caller and is
* not managed by the connection provider ({@link #shutdown} does nothing).
*
* @param url a URL prefix that can be used by the {@link DatabaseLiaison} to identify the type
* of database being accessed by the supplied data sources, this should be one of "jdbc:mysql"
* or "jdbc:postgresql" as those are the only two types of database currently supported.
*/
public DataSourceConnectionProvider (String url, DataSource readSource, DataSource writeSource)
{
_url = url;
_readSource = readSource;
_writeSource = writeSource;
}
// from interface ConnectionProvider
public Connection getConnection (String ident, boolean readOnly)
throws PersistenceException
{
try {
Connection conn;
if (readOnly) {
conn = _readSource.getConnection();
conn.setReadOnly(true);
} else {
conn = _writeSource.getConnection();
}
return conn;
} catch (SQLException sqe) {
throw new PersistenceException(sqe);
}
}
// from interface ConnectionProvider
public void releaseConnection (String ident, boolean readOnly, Connection conn)
{
try {
conn.close();
} catch (Exception e) {
log.warning("Failure closing connection",
"ident", ident, "ro", readOnly, "conn", conn, e);
}
}
// from interface ConnectionProvider
public void connectionFailed (String ident, boolean readOnly, Connection conn,
SQLException error)
{
try {
conn.close();
} catch (Exception e) {
log.warning("Failure closing failed connection",
"ident", ident, "ro", readOnly, "conn", conn, e);
}
}
// from interface ConnectionProvider
public String getURL (String ident)
{
return _url;
}
// from interface ConnectionProvider
public void shutdown ()
{
// nothing doing, the caller has to shutdown the datasources
}
protected String _url;
protected DataSource _readSource, _writeSource;
}
@@ -0,0 +1,215 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.Connection;
import java.sql.SQLException;
/**
* Despite good intentions, JDBC and SQL do not provide a unified interface to all databases. There
* remain idiosyncrasies that must be worked around when making code interact with different
* database servers. The database liaison encapsulates the code needed to straighten out the curves
* and curve out the straights.
*/
public interface DatabaseLiaison
{
/**
* Indicates whether or not this database liaison is the proper liaison for the specified
* database URL.
*
* @return true if we should use this liaison for connections created with the supplied URL,
* false if we should not.
*/
public boolean matchesURL (String url);
/**
* Determines whether or not the supplied SQL exception was caused by a duplicate row being
* inserted into a table with a unique key.
*
* @return true if the exception was caused by the insertion of a duplicate row, false if not.
*/
public boolean isDuplicateRowException (SQLException sqe);
/**
* Determines whether or not the supplied SQL exception is a transient failure, meaning one
* that is not related to the SQL being executed, but instead to the environment at the time of
* execution, like the connection to the database having been lost.
*
* @return true if the exception was thrown due to a transient failure, false if not.
*/
public boolean isTransientException (SQLException sqe);
/**
* Attempts as dialect-agnostic an interface as possible to the ability of certain databases to
* auto-generated numerical values for i.e. key columns; there is MySQL's AUTO_INCREMENT and
* PostgreSQL's DEFAULT nextval(sequence), for example.
*/
public int lastInsertedId (Connection conn, String table, String column)
throws SQLException;
/**
* Initializes the column value auto-generator described in {@link #lastInsertedId}. This
* should be idempotent (meaning the generator may already exist in which case this method
* should have no negative effect like resetting it).
*/
public void createGenerator (Connection conn, String tableName, String columnName,
int initialValue)
throws SQLException;
/**
* Deletes the column value auto-generator described in {@link #lastInsertedId}.
*/
public void deleteGenerator (Connection conn, String tableName, String columnName)
throws SQLException;
/**
* Drops the given column from the given table. Returns true or false if the database did or
* did not report a schema modification.
*/
public boolean dropColumn (Connection conn, String table, String column)
throws SQLException;
/**
* Adds a named index to a table on the given columns. Returns true or false if the database
* did or did not report a schema modification.
*/
public boolean addIndexToTable (
Connection conn, String table, String[] columns, String index, boolean unique)
throws SQLException;
/**
* Drops the named index from the given table.
*/
public void dropIndex (Connection conn, String table, String index)
throws SQLException;
/**
* Adds a primary key to a table of the given name and on the given columns. Returns true or
* false if the database did nor did not report a schema modification.
*/
public void addPrimaryKey (Connection conn, String table, String[] columns)
throws SQLException;
/**
* Deletes the primary key from a table, if it exists.
*/
public void dropPrimaryKey (Connection conn, String table, String pkName)
throws SQLException;
/**
* Adds a column to a table with the given definition. Tests for the previous existence of
* the column iff 'check' is true.
*/
public boolean addColumn (
Connection conn, String table, String column, String definition, boolean check)
throws SQLException;
/**
* Adds a column to a table with the given definition. Tests for the previous existence of
* the column iff 'check' is true.
*/
public boolean addColumn (
Connection conn, String table, String column, ColumnDefinition columnDef, boolean check)
throws SQLException;
/**
* Alter the definition, but not the name, of a given column on a given table. Returns true or
* false if the database did or did not report a schema modification. Any of the nullable,
* unique and defaultValue arguments may be null, in which case the implementation will attempt
* to leave that aspect of the column unchanged.
*/
public boolean changeColumn (
Connection conn, String table, String column, String type, Boolean nullable,
Boolean unique, String defaultValue)
throws SQLException;
/**
* Alter the name, but not the definition, of a given column on a given table. Returns true or
* false if the database did or did not report a schema modification.
*
* @param columnDef the full definition of the new column, including its new name (MySQL
* requires this for a column rename).
*/
public boolean renameColumn (
Connection conn, String table, String oldColumn, String newColumn,
ColumnDefinition columnDef)
throws SQLException;
/**
* Created a new table of the given name with the given column names and column definitions;
* the given set of unique constraints (or null) and the given primary key columns (or null).
* Returns true if the table was successfully created, false if it already existed.
*/
public boolean createTableIfMissing (
Connection conn, String table, String[] columns, ColumnDefinition[] declarations,
String[][] uniqueConstraintColumns, String[] primaryKeyColumns)
throws SQLException;
/**
* Create an SQL string that summarizes a column definition in that format generally
* accepted in table creation and column addition statements, e.g.
* INTEGER UNIQUE NOT NULL DEFAULT 100
*/
public String expandDefinition (ColumnDefinition coldef);
/**
* Returns true if the specified table exists and contains an index of the specified name;
* false if either conditions does not hold true. <em>Note:</em> the names are case sensitive.
*/
public boolean tableContainsIndex (Connection conn, String table, String index)
throws SQLException;
/**
* Returns true if the specified table exists and contains a column with the specified name;
* false if either condition does not hold true. <em>Note:</em> the names are case sensitive.
*/
public boolean tableContainsColumn (Connection conn, String table, String column)
throws SQLException;
/**
* Returns true if the table with the specified name exists, false if it does not.
* <em>Note:</em> the table name is case sensitive.
*/
public boolean tableExists (Connection conn, String name) throws SQLException;
/**
* Drops the given table and returns true if the table exists, else returns false.
* <em>Note:</em> the table name is case sensitive.
*/
public boolean dropTable (Connection conn, String name) throws SQLException;
/**
* Returns the proper SQL to identify a table. Some databases require table names to be quoted.
*/
public String tableSQL (String table);
/**
* Returns the proper SQL to identify a column. Some databases require columns names to be
* quoted.
*/
public String columnSQL (String column);
/**
* Returns the proper SQL to identify an index. Some databases require index names to be
* quoted.
*/
public String indexSQL (String index);
}
@@ -0,0 +1,88 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.Connection;
import java.sql.SQLException;
/**
* The default liaison is used if no other liaison could be matched for a particular database
* connection. It isn't very smart or useful but we need something.
*/
public class DefaultLiaison extends BaseLiaison
{
// from DatabaseLiaison
public boolean matchesURL (String url)
{
return true;
}
// from DatabaseLiaison
public boolean isDuplicateRowException (SQLException sqe)
{
return false;
}
// from DatabaseLiaison
public boolean isTransientException (SQLException sqe)
{
return false;
}
// from DatabaseLiaison
public void createGenerator (Connection conn, String tableName, String columnName, int initValue)
throws SQLException
{
// nothing doing
}
// from DatabaseLiaison
public void deleteGenerator (Connection conn, String tableName, String columnName)
throws SQLException
{
// nothing doing
}
// from DatabaseLiaison
public int lastInsertedId (Connection conn, String table, String column)
throws SQLException
{
return -1;
}
// from DatabaseLiaison
public String columnSQL (String column)
{
return column;
}
// from DatabaseLiaison
public String tableSQL (String table)
{
return table;
}
// from DatabaseLiaison
public String indexSQL (String index)
{
return index;
}
}
@@ -0,0 +1,190 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.SQLException;
import com.samskivert.jdbc.ColumnDefinition;
import com.samskivert.util.ArrayUtil;
/**
* Handles liaison for HSQLDB.
*/
public class HsqldbLiaison extends BaseLiaison
{
// from DatabaseLiaison
public boolean matchesURL (String url)
{
return url.startsWith("jdbc:hsqldb");
}
// from DatabaseLiaison
public String columnSQL (String column)
{
return "\"" + column + "\"";
}
// from DatabaseLiaison
public String tableSQL (String table)
{
return "\"" + table + "\"";
}
// from DatabaseLiaison
public String indexSQL (String index)
{
return "\"" + index + "\"";
}
// from DatabaseLiaison
public void createGenerator (Connection conn, String tableName,
String columnName, int initValue)
throws SQLException
{
// HSQL's IDENTITY() does not create any database entities
}
// from DatabaseLiaison
public void deleteGenerator (Connection conn, String tableName, String columnName)
throws SQLException
{
// HSQL's IDENTITY() does not create any database entities that we need to delete
}
// from DatabaseLiaison
public int lastInsertedId (Connection conn, String table, String column) throws SQLException
{
// HSQL does not keep track of per-table-and-column insertion data, so we are pretty much
// going on blind faith here that we're fetching the right ID. In the overwhelming number
// of cases that will be so, but it's still not pretty.
Statement stmt = null;
try {
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("call IDENTITY()");
return rs.next() ? rs.getInt(1) : -1;
} finally {
JDBCUtil.close(stmt);
}
}
// from DatabaseLiaison
public boolean isTransientException (SQLException sqe)
{
return false; // no known transient exceptions for HSQLDB
}
// from DatabaseLiaison
public boolean isDuplicateRowException (SQLException sqe)
{
// Violation of unique constraint SYS_PK_51: duplicate value(s) for column(s) FOO
String msg = sqe.getMessage();
return (msg != null && msg.indexOf("duplicate value(s)") != -1);
}
// BaseLiaison's implementation of table creation accepts unique constraints both as
// part of the column definition and as a separate argument, and merrily passes this
// duality onto the database. Postgres and MySQL both handle this fine but HSQL seems
// to simply not allow uniqueness in the column definitions. So, for HSQL, we transfer
// uniqueness from the ColumnDefinitions to the uniqueConstraintColumns before we pass
// it in to the super implementation.
//
// TODO: Consider making this the general MO instead of a subclass override. In fact
// it may be that uniqueness should be removed from ColumnDefinition.
@Override // from DatabaseLiaison
public boolean createTableIfMissing (
Connection conn, String table, String[] columns, ColumnDefinition[] definitions,
String[][] uniqueConstraintColumns, String[] primaryKeyColumns)
throws SQLException
{
if (columns.length != definitions.length) {
throw new IllegalArgumentException("Column name and definition number mismatch");
}
// make a set of unique constraints already provided
Set<List<String>> uColSet = new HashSet<List<String>>();
if (uniqueConstraintColumns != null) {
for (String[] uCols : uniqueConstraintColumns) {
uColSet.add(Arrays.asList(uCols));
}
}
// go through the columns and find any that are unique; these we replace with a
// non-unique variant, and instead add a new entry to the table unique constraint
ColumnDefinition[] newDefinitions = new ColumnDefinition[definitions.length];
for (int ii = 0; ii < definitions.length; ii ++) {
ColumnDefinition def = definitions[ii];
if (def.unique) {
// let's be nice and not mutate the caller's object
newDefinitions[ii] = new ColumnDefinition(
def.type, def.nullable, false, def.defaultValue);
// if a uniqueness constraint for this column was not in the
// uniqueConstraintColumns parameter, add such an entry
if (!uColSet.contains(Collections.singletonList(columns[ii]))) {
String[] newConstraint = new String[] { columns[ii] };
uniqueConstraintColumns = (uniqueConstraintColumns == null) ?
new String[][] { newConstraint } :
ArrayUtil.append(uniqueConstraintColumns, newConstraint);
}
} else {
newDefinitions[ii] = def;
}
}
// now call the real implementation with our modified data
return super.createTableIfMissing(
conn, table, columns, newDefinitions, uniqueConstraintColumns, primaryKeyColumns);
}
@Override // from DatabaseLiaison
protected String expandDefinition (
String type, boolean nullable, boolean unique, String defaultValue)
{
StringBuilder builder = new StringBuilder(type);
// append the default value if one was specified
if (defaultValue != null) {
if ("IDENTITY".equals(defaultValue)) {
// this is a blatant hack, we need this method to join Depot's SQLBuilder
builder.append(" GENERATED BY DEFAULT AS IDENTITY (START WITH 1)");
} else {
builder.append(" DEFAULT ").append(defaultValue);
}
}
if (!nullable) {
builder.append(" NOT NULL");
}
if (unique) {
throw new IllegalArgumentException("HSQL can't deal with column uniqueness here");
}
return builder.toString();
}
}
@@ -0,0 +1,653 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import com.samskivert.io.PersistenceException;
import com.samskivert.util.StringUtil;
import static com.samskivert.Log.log;
/**
* A repository for JDBC related utility functions.
*/
public class JDBCUtil
{
/** Used for {@link #batchQuery}. */
public interface BatchProcessor
{
/**
* Called for each row returned during our batch query. Do not advance the result set,
* simply query its values with the get methods.
*/
public void process (ResultSet row)
throws SQLException;
}
/**
* Closes the supplied JDBC statement and gracefully handles being passed null (by doing
* nothing).
*/
public static void close (Statement stmt)
throws SQLException
{
if (stmt != null) {
stmt.close();
}
}
/**
* Closes the supplied JDBC connection and gracefully handles being passed null (by doing
* nothing).
*/
public static void close (Connection conn)
throws SQLException
{
if (conn != null) {
conn.close();
}
}
/**
* Wraps the given connection in a proxied instance that will add all statements returned by
* methods called on the proxy (such as {@link Connection#createStatement}) to the supplied
* list. Thus you can create the proxy, pass the proxy to code that creates and uses statements
* and then close any statements created by the code that operated on that Connection before
* returning it to a pool, for example.
*/
public static Connection makeCollector (final Connection conn, final List<Statement> stmts)
{
return (Connection)Proxy.newProxyInstance(
Connection.class.getClassLoader(), PROXY_IFACES, new InvocationHandler() {
public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(conn, args);
if (result instanceof Statement) {
stmts.add((Statement)result);
}
return result;
}
});
}
/**
* Calls <code>stmt.executeUpdate()</code> on the supplied statement, checking to see that it
* returns the expected update count and throwing a persistence exception if it does not.
*/
public static void checkedUpdate (PreparedStatement stmt, int expectedCount)
throws SQLException, PersistenceException
{
int modified = stmt.executeUpdate();
if (modified != expectedCount) {
String err = "Statement did not modify expected number of rows [stmt=" + stmt +
", expected=" + expectedCount + ", modified=" + modified + "]";
throw new PersistenceException(err);
}
}
/**
* Calls <code>stmt.executeUpdate()</code> on the supplied statement with the supplied query,
* checking to see that it returns the expected update count and throwing a persistence
* exception if it does not.
*/
public static void checkedUpdate (
Statement stmt, String query, int expectedCount)
throws SQLException, PersistenceException
{
int modified = stmt.executeUpdate(query);
if (modified != expectedCount) {
String err = "Statement did not modify expected number of rows [stmt=" + stmt +
", expected=" + expectedCount + ", modified=" + modified + "]";
throw new PersistenceException(err);
}
}
/**
* Calls <code>stmt.executeUpdate()</code> on the supplied statement, checking to see that it
* returns the expected update count and logging a warning if it does not.
*/
public static void warnedUpdate (PreparedStatement stmt, int expectedCount)
throws SQLException
{
int modified = stmt.executeUpdate();
if (modified != expectedCount) {
log.warning("Statement did not modify expected number of rows", "stmt", stmt,
"expected", expectedCount, "modified", modified);
}
}
/**
* Issues a query with a potentially large number of keys in batches. For example, you might
* have 10,000 ids that you wish to use in an "in" clause, but don't trust the database to be
* smart about optimizing that many keys, so instead you use batchQuery like so:
* <pre>
* Collection<Integer> keys = ...;
* String query = "select NAME from USERS where USER_ID in (#KEYS)";
* JDBCUtil.BatchProcessor proc = new JDBCUtil.BatchProcessor() {
* public void process (ResultSet row) {
* String name = rs.getString(1);
* // do whatever with name
* }
* };
* JDBCUtil.batchQuery(conn, query, keys, false, 500, proc);
* </pre>
*
* @param query the SQL query to run for each batch with the string <code>#KEYS#</code> in the
* place where the batch of keys should be substituted.
* @param escapeKeys if true, {@link #escape} will be called on each key to escape any
* dangerous characters and wrap the key in quotes.
* @param batchSize the number of keys at a time to substitute in for <code>#KEYS#</code>.
*/
public static void batchQuery (Connection conn, String query, Collection<?> keys,
boolean escapeKeys, int batchSize, BatchProcessor processor)
throws SQLException
{
Statement stmt = conn.createStatement();
try {
Iterator<?> itr = keys.iterator();
while (itr.hasNext()) {
// group one batch of keys together
StringBuilder buf = new StringBuilder();
for (int ii = 0; ii < batchSize && itr.hasNext(); ii++) {
if (ii > 0) {
buf.append(",");
}
String key = String.valueOf(itr.next());
buf.append(escapeKeys ? escape(key) : key);
}
// issue the query with that batch
String squery = query.replace("#KEYS#", buf.toString());
ResultSet rs = stmt.executeQuery(squery);
while (rs.next()) {
processor.process(rs);
}
}
} finally {
close(stmt);
}
}
/**
* Calls <code>stmt.executeUpdate()</code> on the supplied statement with the supplied query,
* checking to see that it returns the expected update count and logging a warning if it does
* not.
*/
public static void warnedUpdate (
Statement stmt, String query, int expectedCount)
throws SQLException
{
int modified = stmt.executeUpdate(query);
if (modified != expectedCount) {
log.warning("Statement did not modify expected number of rows", "stmt", stmt,
"expected", expectedCount, "modified", modified);
}
}
/**
* Escapes any single quotes in the supplied text and wraps it in single quotes to make it safe
* for embedding into a database query.
*/
public static String escape (String text)
{
text = text.replace("\\", "\\\\");
return "'" + text.replace("'", "\\'") + "'";
}
/**
* Escapes a list of values, separating the escaped values by commas. See {@link
* #escape(String)}.
*/
public static String escape (Object[] values)
{
StringBuilder buf = new StringBuilder();
for (int ii = 0; ii < values.length; ii++) {
if (ii > 0) {
buf.append(", ");
}
buf.append(escape(String.valueOf(values[ii])));
}
return buf.toString();
}
/**
* Many databases simply fail to handle Unicode text properly and this routine provides a
* common workaround which is to represent a UTF-8 string as an ISO-8859-1 string. If you don't
* need to use the database's collation routines, this allows you to do pretty much exactly
* what you want at the expense of having to jigger and dejigger every goddamned string that
* might contain multibyte characters every time you access the database. Three cheers for
* progress!
*/
public static String jigger (String text)
{
if (text == null) {
return null;
}
try {
return new String(text.getBytes("UTF8"), "8859_1");
} catch (UnsupportedEncodingException uee) {
log.warning("Jigger failed", uee);
return text;
}
}
/**
* Reverses {@link #jigger}.
*/
public static String unjigger (String text)
{
if (text == null) {
return null;
}
try {
return new String(text.getBytes("8859_1"), "UTF8");
} catch (UnsupportedEncodingException uee) {
log.warning("Unjigger failed", uee);
return text;
}
}
/**
* Utility method to jigger the specified string so that it's safe to use in a regular
* Statement.
*/
public static String safeJigger (String text)
{
return jigger(text).replace("'", "\\'");
}
/**
* Used to programatically create a database table. Does nothing if the table already exists.
*
* @return true if the table was created, false if it already existed.
*/
public static boolean createTableIfMissing (
Connection conn, String table, String[] definition, String postamble)
throws SQLException
{
if (tableExists(conn, table)) {
return false;
}
Statement stmt = conn.createStatement();
try {
stmt.executeUpdate("create table " + table +
"(" + StringUtil.join(definition, ", ") + ") " + postamble);
} finally {
close(stmt);
}
log.info("Database table '" + table + "' created.");
return true;
}
/**
* Returns true if the table with the specified name exists, false if it does
* not. <em>Note:</em> the table name is case sensitive.
*/
public static boolean tableExists (Connection conn, String name)
throws SQLException
{
boolean matched = false;
ResultSet rs = conn.getMetaData().getTables("", "", name, null);
while (rs.next()) {
String tname = rs.getString("TABLE_NAME");
if (name.equals(tname)) {
matched = true;
}
}
return matched;
}
/**
* Returns true if the table with the specified name exists and contains a column with the
* specified name, false if either condition does not hold true. <em>Note:</em> the names are
* case sensitive.
*/
public static boolean tableContainsColumn (Connection conn, String table, String column)
throws SQLException
{
boolean matched = false;
ResultSet rs = conn.getMetaData().getColumns("", "", table, column);
while (rs.next()) {
String tname = rs.getString("TABLE_NAME");
String cname = rs.getString("COLUMN_NAME");
if (tname.equals(table) && cname.equals(column)) {
matched = true;
}
}
return matched;
}
/**
* Returns true if the index on the specified column exists for the specified table, false if
* it does not. Optionally you can specifiy a non null index name, and the table will be
* checked to see if it contains that specifically named index. <em>Note:</em> the names are
* case sensitive.
*/
public static boolean tableContainsIndex (
Connection conn, String table, String column, String index)
throws SQLException
{
boolean matched = false;
ResultSet rs = conn.getMetaData().getIndexInfo("", "", table, false, true);
while (rs.next()) {
String tname = rs.getString("TABLE_NAME");
String cname = rs.getString("COLUMN_NAME");
String iname = rs.getString("INDEX_NAME");
if (index == null) {
if (tname.equals(table) && cname.equals(column)) {
matched = true;
}
} else if (index.equals(iname)) {
matched = true;
}
}
return matched;
}
/**
* Returns true if the specified table contains a primary key on the specified column.
*/
public static boolean tableContainsPrimaryKey (Connection conn, String table, String column)
throws SQLException
{
boolean matched = false;
ResultSet rs = conn.getMetaData().getPrimaryKeys("", "", table);
while (rs.next()) {
String tname = rs.getString("TABLE_NAME");
String cname = rs.getString("COLUMN_NAME");
if (tname.equals(table) && cname.equals(column)) {
matched = true;
}
}
return matched;
}
/**
* Returns the name of the index for the specified column in the specified table.
*/
public static String getIndexName (Connection conn, String table,
String column)
throws SQLException
{
ResultSet rs = conn.getMetaData().getIndexInfo("", "", table, false, true);
while (rs.next()) {
String tname = rs.getString("TABLE_NAME");
String cname = rs.getString("COLUMN_NAME");
String iname = rs.getString("INDEX_NAME");
if (tname.equals(table) && cname.equals(column)) {
return iname;
}
}
return null;
}
/**
* Returns the type (as specified in {@link java.sql.Types} for the specified column in the
* specified table.
*/
public static int getColumnType (Connection conn, String table,
String column)
throws SQLException
{
ResultSet rs = getColumnMetaData(conn, table, column);
try {
return rs.getInt("DATA_TYPE");
} finally {
rs.close();
}
}
/**
* Determines whether or not the specified column accepts null values.
*
* @return true if the column accepts null values, false if it does not (or its nullability is
* unknown)
*/
public static boolean isColumnNullable (Connection conn, String table,
String column)
throws SQLException
{
ResultSet rs = getColumnMetaData(conn, table, column);
try {
return rs.getString("IS_NULLABLE").equals("YES");
} finally {
rs.close();
}
}
/**
* Returns the size for the specified column in the specified table. For char or date types
* this is the maximum number of characters, for numeric or decimal types this is the
* precision.
*/
public static int getColumnSize (Connection conn, String table, String column)
throws SQLException
{
ResultSet rs = getColumnMetaData(conn, table, column);
try {
return rs.getInt("COLUMN_SIZE");
} finally {
rs.close();
}
}
/**
* Returns a string representation of the default value for the specified column in the
* specified table. This may be null.
*/
public static String getColumnDefaultValue (Connection conn, String table, String column)
throws SQLException
{
ResultSet rs = getColumnMetaData(conn, table, column);
try {
return rs.getString("COLUMN_DEF");
} finally {
rs.close();
}
}
/**
* Adds a column (with name 'cname' and definition 'cdef') to the specified table.
*
* @param afterCname (optional) the name of the column after which to add the new column.
*
* @return true if the column was added, false if it already existed.
*/
public static boolean addColumn (
Connection conn, String table, String cname, String cdef, String afterCname)
throws SQLException
{
if (tableContainsColumn(conn, table, cname)) {
// Log.info("Database table '" + table + "' already has column '" + cname + "'.");
return false;
}
String update = "ALTER TABLE " + table + " ADD COLUMN " + cname + " " + cdef;
if (afterCname != null) {
update += " AFTER " + afterCname;
}
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(update);
stmt.executeUpdate();
} finally {
close(stmt);
}
log.info("Database column '" + cname + "' added to table '" + table + "'.");
return true;
}
/**
* Changes a column's definition. Takes a full column definition 'cdef' (including the name of
* the column) with which to replace the specified column 'cname'.
*
* NOTE: A handy thing you can do with this is to rename a column by providing a column
* definition that has a different name, but the same column type.
*/
public static void changeColumn (Connection conn, String table, String cname, String cdef)
throws SQLException
{
String update = "ALTER TABLE " + table + " CHANGE " + cname + " " + cdef;
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(update);
stmt.executeUpdate();
} finally {
close(stmt);
}
log.info("Database column '" + cname + "' of table '" + table +
"' modified to have this def '" + cdef + "'.");
}
/**
* Removes a column from the specified table.
*
* @return true if the column was dropped, false if it did not exist in the first place.
*/
public static boolean dropColumn (Connection conn, String table, String cname)
throws SQLException
{
if (!tableContainsColumn(conn, table, cname)) {
return false;
}
String update = "ALTER TABLE " + table + " DROP COLUMN " + cname;
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(update);
if (stmt.executeUpdate() == 1) {
log.info("Database index '" + cname + "' removed from table '" + table + "'.");
}
} finally {
close(stmt);
}
return true;
}
/**
* Removes a named index from the specified table.
*
* @return true if the index was dropped, false if it did not exist in the first place.
*/
public static boolean dropIndex (Connection conn, String table, String cname, String iname)
throws SQLException
{
if (!tableContainsIndex(conn, table, cname, iname)) {
return false;
}
String update = "ALTER TABLE " + table + " DROP INDEX " + iname;
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(update);
if (stmt.executeUpdate() == 1) {
log.info("Database index '" + iname + "' removed from table '" + table + "'.");
}
} finally {
close(stmt);
}
return true;
}
/**
* Removes the primary key from the specified table.
*/
public static void dropPrimaryKey (Connection conn, String table)
throws SQLException
{
String update = "ALTER TABLE " + table + " DROP PRIMARY KEY";
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(update);
if (stmt.executeUpdate() == 1) {
log.info("Database primary key removed from '" + table + "'.");
}
} finally {
close(stmt);
}
}
/**
* Adds an index on the specified column (cname) to the specified table. Optionally supply an
* index name, otherwise the index is named after the column.
*
* @return true if the index was added, false if it already existed.
*/
public static boolean addIndexToTable (
Connection conn, String table, String cname, String iname)
throws SQLException
{
if (tableContainsIndex(conn, table, cname, iname)) {
// Log.info("Database table '" + table + "' already has an index " +
// "on column '" + cname + "'" +
// (iname != null ? " named '" + iname + "'." : "."));
return false;
}
String idx_name = (iname != null ? iname : cname);
String update = "CREATE INDEX " + idx_name + " on " + table + "(" + cname + ")";
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(update);
stmt.executeUpdate();
} finally {
close(stmt);
}
log.info("Database index '" + idx_name + "' added to table '" + table + "'");
return true;
}
/**
* Helper function for {@link #getColumnType}, etc.
*/
protected static ResultSet getColumnMetaData (Connection conn, String table, String column)
throws SQLException
{
ResultSet rs = conn.getMetaData().getColumns("", "", table, column);
while (rs.next()) {
String tname = rs.getString("TABLE_NAME");
String cname = rs.getString("COLUMN_NAME");
if (tname.equals(table) && cname.equals(column)) {
return rs;
}
}
throw new SQLException("Table or Column not defined. [table=" + table +
", col=" + column + "].");
}
/** Used by {@link #makeCollectingConnection}. */
protected static final Class<?>[] PROXY_IFACES = { Connection.class };
}
@@ -0,0 +1,341 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.*;
import java.util.ArrayList;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.jora.*;
/**
* The JORA repository simplifies the process of building persistence
* services that make use of the JORA object relational mapping package.
*
* @see com.samskivert.jdbc.jora.Table
*/
public abstract class JORARepository extends SimpleRepository
{
/**
* Creates and initializes a JORA repository which will access the
* database identified by the supplied database identifier.
*
* @param provider the connection provider which will be used to
* obtain our database connection.
* @param dbident the identifier of the database that will be accessed
* by this repository.
*/
public JORARepository (ConnectionProvider provider, String dbident)
{
super(provider, dbident);
// create our tables
createTables();
}
/**
* Inserts the supplied object into the specified table.
*
* @return a call to {@link DatabaseLiaison#lastInsertedId} made
* immediately following the insert.
*/
protected <T> int insert (final Table<T> table, final T object)
throws PersistenceException
{
return executeUpdate(new Operation<Integer>() {
public Integer invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
table.insert(conn, object);
return liaison.lastInsertedId(conn, table.getName(), "TODO");
}
});
}
/**
* Updates the supplied object in the specified table.
*
* @return the number of rows modified by the update.
*/
protected <T> int update (final Table<T> table, final T object)
throws PersistenceException
{
return executeUpdate(new Operation<Integer>() {
public Integer invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.update(conn, object);
}
});
}
/**
* Updates fields specified by the supplied field mask in the supplied
* object in the specified table.
*
* @return the number of rows modified by the update.
*/
protected <T> int update (final Table<T> table, final T object,
final FieldMask mask)
throws PersistenceException
{
return executeUpdate(new Operation<Integer>() {
public Integer invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.update(conn, object, mask);
}
});
}
/**
* Loads all objects from the specified table that match the supplied
* query.
*/
protected <T> ArrayList<T> loadAll (final Table<T> table,
final String query)
throws PersistenceException
{
return execute(new Operation<ArrayList<T>>() {
public ArrayList<T> invoke (
Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.select(conn, query).toArrayList();
}
});
}
/**
* Loads all objects from the specified table that match the supplied
* query, joining with the supplied auxiliary table(s).
*/
protected <T> ArrayList<T> loadAll (
final Table<T> table, final String auxtable, final String query)
throws PersistenceException
{
return execute(new Operation<ArrayList<T>>() {
public ArrayList<T> invoke (
Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.select(conn, auxtable, query).toArrayList();
}
});
}
/**
* Loads all objects from the specified table that match the supplied
* example.
*/
protected <T> ArrayList<T> loadAllByExample (
final Table<T> table, final T example)
throws PersistenceException
{
return execute(new Operation<ArrayList<T>>() {
public ArrayList<T> invoke (
Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.queryByExample(conn, example).toArrayList();
}
});
}
/**
* Loads all objects from the specified table that match the supplied
* example.
*/
protected <T> ArrayList<T> loadAllByExample (
final Table<T> table, final T example, final FieldMask mask)
throws PersistenceException
{
return execute(new Operation<ArrayList<T>>() {
public ArrayList<T> invoke (
Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.queryByExample(conn, example, mask).toArrayList();
}
});
}
/**
* Loads a single object from the specified table that matches the
* supplied query. <em>Note:</em> the query should match one or zero
* records, not more.
*/
protected <T> T load (final Table<T> table, final String query)
throws PersistenceException
{
return execute(new Operation<T>() {
public T invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.select(conn, query).get();
}
});
}
/**
* Loads a single object from the specified table that matches the supplied
* query, joining with the supplied auxiliary table(s). <em>Note:</em> the
* query should match one or zero records, not more.
*/
protected <T> T load (
final Table<T> table, final String auxtable, final String query)
throws PersistenceException
{
return execute(new Operation<T>() {
public T invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.select(conn, auxtable, query).get();
}
});
}
/**
* Loads a single object from the specified table that matches the
* supplied example. <em>Note:</em> the query should match one or zero
* records, not more.
*/
protected <T> T loadByExample (final Table<T> table, final T example)
throws PersistenceException
{
return execute(new Operation<T>() {
public T invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.queryByExample(conn, example).get();
}
});
}
/**
* Loads a single object from the specified table that matches the
* supplied example. <em>Note:</em> the query should match one or zero
* records, not more.
*/
protected <T> T loadByExample (
final Table<T> table, final T example, final FieldMask mask)
throws PersistenceException
{
return execute(new Operation<T>() {
public T invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.queryByExample(conn, example, mask).get();
}
});
}
/**
* First attempts to update the supplied object and if that modifies
* zero rows, inserts the object into the specified table. The table
* must be configured to store items of the supplied type.
*
* @return -1 if the object was updated, the last inserted id if it was
* inserted.
*/
protected <T> int store (final Table<T> table, final T object)
throws PersistenceException
{
return executeUpdate(new Operation<Integer>() {
public Integer invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
if (table.update(conn, object) == 0) {
table.insert(conn, object);
return liaison.lastInsertedId(conn, table.getName(), "TODO");
}
return -1;
}
});
}
/**
* Updates the specified field in the supplied object (which must
* correspond to the supplied table).
*
* @return the number of rows modified by the update.
*/
protected <T> int updateField (
final Table<T> table, final T object, String field)
throws PersistenceException
{
final FieldMask mask = table.getFieldMask();
mask.setModified(field);
return executeUpdate(new Operation<Integer>() {
public Integer invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.update(conn, object, mask);
}
});
}
/**
* Updates the specified fields in the supplied object (which must
* correspond to the supplied table).
*
* @return the number of rows modified by the update.
*/
protected <T> int updateFields (
final Table<T> table, final T object, String[] fields)
throws PersistenceException
{
final FieldMask mask = table.getFieldMask();
for (int ii = 0; ii < fields.length; ii++) {
mask.setModified(fields[ii]);
}
return executeUpdate(new Operation<Integer>() {
public Integer invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.update(conn, object, mask);
}
});
}
/**
* Deletes the specified object from the table.
*
* @return the number of rows deleted.
*/
protected <T> int delete (final Table<T> table, final T object)
throws PersistenceException
{
return executeUpdate(new Operation<Integer>() {
public Integer invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.delete(conn, object);
}
});
}
/**
* During construction, this function will be called to give the repository
* implementation the opportunity to create its table objects.
*/
protected abstract void createTables ();
}
@@ -0,0 +1,95 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import static com.samskivert.Log.log;
/**
* The liaison registry provides access to the appropriate database liaison implementation for a
* particular database connection.
*/
public class LiaisonRegistry
{
/**
* Fetch the appropriate database liaison for the supplied URL, which should be the same string
* that would be used to configure a connection to the database.
*/
public static DatabaseLiaison getLiaison (String url)
{
// see if we already have a liaison mapped for this connection
DatabaseLiaison liaison = _mappings.get(url);
if (liaison == null) {
// scan the list looking for a matching liaison
for (DatabaseLiaison candidate : _liaisons) {
if (candidate.matchesURL(url)) {
liaison = candidate;
break;
}
}
// if we didn't find a matching liaison, use the default
if (liaison == null) {
log.warning("Unable to match liaison for database. Using default.", "url", url);
liaison = new DefaultLiaison();
}
// map this URL to this liaison
_mappings.put(url, liaison);
}
return liaison;
}
/**
* Fetch the appropriate database liaison for the supplied database connection.
*/
public static DatabaseLiaison getLiaison (Connection conn)
throws SQLException
{
return getLiaison(conn.getMetaData().getURL());
}
protected static void registerLiaisonClass (Class<? extends DatabaseLiaison> lclass)
{
// create a new instance and stick it on our list
try {
_liaisons.add(lclass.newInstance());
} catch (Exception e) {
log.warning("Unable to instantiate liaison", "class", lclass.getName(), "error", e);
}
}
protected static ArrayList<DatabaseLiaison> _liaisons = new ArrayList<DatabaseLiaison>();
protected static Map<String,DatabaseLiaison> _mappings = new HashMap<String,DatabaseLiaison>();
// register our liaison classes
static {
registerLiaisonClass(MySQLLiaison.class);
registerLiaisonClass(PostgreSQLLiaison.class);
registerLiaisonClass(HsqldbLiaison.class);
}
}
@@ -0,0 +1,159 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.*;
import static com.samskivert.Log.log;
/**
* A database liaison for the MySQL database.
*/
public class MySQLLiaison extends BaseLiaison
{
// from DatabaseLiaison
public boolean matchesURL (String url)
{
return url.startsWith("jdbc:mysql");
}
// from DatabaseLiaison
public boolean isDuplicateRowException (SQLException sqe)
{
String msg = sqe.getMessage();
return (msg != null && msg.indexOf("Duplicate entry") != -1);
}
// from DatabaseLiaison
public boolean isTransientException (SQLException sqe)
{
String msg = sqe.getMessage();
return (msg != null && (msg.indexOf("Lost connection") != -1 ||
msg.indexOf("link failure") != -1 ||
msg.indexOf("Broken pipe") != -1));
}
// from DatabaseLiaison
public void createGenerator (Connection conn, String tableName, String columnName, int initValue)
throws SQLException
{
// TODO: is there any way we can set the initial AUTO_INCREMENT value?
}
// from DatabaseLiaison
public void deleteGenerator (Connection conn, String tableName, String columnName)
throws SQLException
{
// AUTO_INCREMENT does not create any database entities that we need to delete
}
// from DatabaseLiaison
public int lastInsertedId (Connection conn, String table, String column) throws SQLException
{
// MySQL does not keep track of per-table-and-column insertion data, so we are pretty much
// going on blind faith here that we're fetching the right ID. In the overwhelming number
// of cases that will be so, but it's still not pretty.
Statement stmt = null;
try {
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select LAST_INSERT_ID()");
return rs.next() ? rs.getInt(1) : -1;
} finally {
JDBCUtil.close(stmt);
}
}
@Override // from BaseLiaison
public boolean addIndexToTable (
Connection conn, String table, String[] columns, String ixName, boolean unique)
throws SQLException
{
if (tableContainsIndex(conn, table, ixName)) {
return false;
}
// MySQL's "CREATE INDEX" is buggy, it actually changes the case of the table names you're
// working with. Luckily (?) ALTER TABLE ADD INDEX works, so we do that here.
StringBuilder update = new StringBuilder("ALTER TABLE ").append(table).append(" ADD ");
if (unique) {
update.append("UNIQUE ");
}
update.append("INDEX ").append(indexSQL(ixName)).append(" (");
for (int ii = 0; ii < columns.length; ii ++) {
if (ii > 0) {
update.append(", ");
}
update.append(columnSQL(columns[ii]));
}
update.append(")");
executeQuery(conn, update.toString());
log.info("Database index '" + ixName + "' added to table '" + table + "'");
return true;
}
@Override // from BaseLiaison
public void dropIndex (Connection conn, String table, String index)
throws SQLException
{
executeQuery(conn, "ALTER TABLE " + tableSQL(table) + " DROP INDEX " + columnSQL(index));
}
@Override // from BaseLiaison
public void dropPrimaryKey (Connection conn, String table, String pkName)
throws SQLException
{
executeQuery(conn, "ALTER TABLE " + tableSQL(table) + " DROP PRIMARY KEY");
}
@Override // from BaseLiaison
public boolean renameColumn (Connection conn, String table, String oldColumnName,
String newColumnName, ColumnDefinition newColumnDef)
throws SQLException
{
if (!tableContainsColumn(conn, table, oldColumnName)) {
return false;
}
executeQuery(conn, "ALTER TABLE " + table + " CHANGE " + oldColumnName + " " +
newColumnName + " " + expandDefinition(newColumnDef));
log.info("Renamed column '" + oldColumnName + "' on table '" + table + "' to '" +
newColumnName + "'");
return true;
}
// from DatabaseLiaison
public String columnSQL (String column)
{
return "`" + column + "`";
}
// from DatabaseLiaison
public String tableSQL (String table)
{
return "`" + table + "`";
}
// from DatabaseLiaison
public String indexSQL (String index)
{
return "`" + index + "`";
}
}
@@ -0,0 +1,155 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001 Michael Bayne
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.*;
import static com.samskivert.Log.log;
/**
* A database liaison for the MySQL database.
*/
public class PostgreSQLLiaison extends BaseLiaison
{
// from DatabaseLiaison
public boolean matchesURL (String url)
{
return url.startsWith("jdbc:postgresql");
}
// from DatabaseLiaison
public boolean isDuplicateRowException (SQLException sqe)
{
String msg = sqe.getMessage();
return (msg != null && msg.indexOf("duplicate key") != -1);
}
// from DatabaseLiaison
public boolean isTransientException (SQLException sqe)
{
// TODO: Add more error messages here as we encounter them.
String msg = sqe.getMessage();
return (msg != null &&
msg.indexOf("An I/O error occured while sending to the backend") != -1);
}
// from DatabaseLiaison
public int lastInsertedId (Connection conn, String table, String column) throws SQLException
{
// PostgreSQL's support for auto-generated ID's comes in the form of appropriately named
// sequences and DEFAULT nextval(sequence) modifiers in the ID columns. To get the next ID,
// we use the currval() method which is set in a database sessions when any given sequence
// is incremented.
Statement stmt = conn.createStatement();
try {
ResultSet rs = stmt.executeQuery(
"select currval('\"" + table + "_" + column + "_seq\"')");
return rs.next() ? rs.getInt(1) :-1;
} finally {
JDBCUtil.close(stmt);
}
}
// from DatabaseLiaison
public void createGenerator (Connection conn, String tableName, String columnName, int initValue)
throws SQLException
{
if (initValue == 1) {
return; // that's the default! yay, do nothing
}
String seqname = "\"" + tableName + "_" + columnName + "_seq\"";
Statement stmt = conn.createStatement();
try {
stmt.executeQuery("select setval('" + seqname + "', " + initValue + ", false)");
} finally {
JDBCUtil.close(stmt);
}
log.info("Initial value of " + seqname + " set to " + initValue + ".");
}
// from DatabaseLiaison
public void deleteGenerator (Connection conn, String table, String column)
throws SQLException
{
executeQuery(conn, "drop sequence if exists \"" + table + "_" + column + "_seq\"");
}
@Override // from DatabaseLiaison
public boolean changeColumn (Connection conn, String table, String column, String type,
Boolean nullable, Boolean unique, String defaultValue)
throws SQLException
{
StringBuilder lbuf = new StringBuilder();
if (type != null) {
executeQuery(
conn, "ALTER TABLE " + tableSQL(table) + " ALTER COLUMN " + columnSQL(column) +
" TYPE " + type);
lbuf.append("type=").append(type);
}
if (nullable != null) {
executeQuery(
conn, "ALTER TABLE " + tableSQL(table) + " ALTER COLUMN " + columnSQL(column) +
" " + (nullable ? "DROP NOT NULL" : "SET NOT NULL"));
if (lbuf.length() > 0) {
lbuf.append(", ");
}
lbuf.append("nullable=").append(nullable);
}
if (unique != null) {
// TODO: I think this requires ALTER TABLE DROP CONSTRAINT and so on
if (lbuf.length() > 0) {
lbuf.append(", ");
}
lbuf.append("unique=").append(unique).append(" (not implemented yet)");
}
if (defaultValue != null) {
executeQuery(
conn, "ALTER TABLE " + tableSQL(table) + " ALTER COLUMN " + columnSQL(column) +
" " + (defaultValue.length() > 0 ? "SET DEFAULT " + defaultValue : "DROP DEFAULT"));
if (lbuf.length() > 0) {
lbuf.append(", ");
}
lbuf.append("defaultValue=").append(defaultValue);
}
log.info("Database column '" + column + "' of table '" + table + "' modified to have " +
"definition [" + lbuf + "].");
return true;
}
// from DatabaseLiaison
public String columnSQL (String column)
{
return "\"" + column + "\"";
}
// from DatabaseLiaison
public String tableSQL (String table)
{
return "\"" + table + "\"";
}
// from DatabaseLiaison
public String indexSQL (String index)
{
return "\"" + index + "\"";
}
}
@@ -0,0 +1,119 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.Connection;
import java.sql.SQLException;
import com.samskivert.io.PersistenceException;
/**
* The repository class provides basic functionality upon which to build
* an interface to a repository of information stored in a database (a
* table or set of tables) that is accessed via JDBC.
*
* <p> It is expected that the repository class will encapsulate all
* database access for a particular table or set of tables. The interface
* provided to the rest of the application will involve only the
* application object model. For example:
*
* <pre>
* public class PeopleRepository extends SimpleRepository
* {
* public Person getPerson (int personid);
* public Person[] getPeopleByFirstName (String firstName);
* public void updatePerson (Person person);
* }
* </pre>
*
* <p> It is probably also desirable to catch SQL exceptions and wrap them
* in the <code>PersistenceException</code> class that is provided by this
* package.
*
* <p> The repository comes in a few flavors depending on the needs of the
* persistence services being developed:
*
* <ul>
* <li>{@link SimpleRepository}: The simple repository is used by services
* that need access to a single JDBC connection to perform their database
* operations.</li>
*
* <li>{@link JORARepository}: JORA repository is used by services that
* wish to make use of the JORA Java/RDBMS interoperability package.</li>
*
* <li> Because the repository provides a unified interface to a
* particular persistence service, it is conceivable that it would need to
* talk to multiple databases to provide those services. Presently, the
* repository only supports a single connection, but if the need arose,
* implementing a repository flavor that supported multiple connections
* would be the proper solution.</li>
* </ul>
*/
public class Repository
{
/**
* Creates and initializes the repository.
*
* @param provider the connection provider which will be used to
* obtain our database connection.
*/
public Repository (ConnectionProvider provider)
{
_provider = provider;
}
/**
* Database operations should be encapsulated in instances of this
* class and then provided to the repository for invocation. This
* allows the repository to manage transaction commits for you as well
* as for it to automatically retry an operation if the connection
* failed for some transient reason.
*/
public interface Operation<V>
{
/**
* Invokes code that performs one or more database operations, all
* of which will be encapsulated in a single transaction (which
* can be retried in the event of a transient failure).
*
* @param conn the database connection on which the operations
* will be performed.
* @param liaison a database liaison for the supplied connection
* which can be used to determine things for which there is no
* standard way to determine via JDBC.
*
* @exception SQLException if thrown, this will be wrapped in a
* {@link PersistenceException} before being passed up to the
* operation invoker.
* @exception PersistenceException can be thrown if something goes
* awry when executing the operation. Note that the operation will
* not be retried if a persistence exception is thrown. Such
* exceptions are assumed to be application specific and not
* indicative of a basic JDBC failure. The transaction
* <em>will</em> be rolled back in such cases, however.
*/
public V invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException;
}
/** Our database connection provider. */
protected ConnectionProvider _provider;
}
@@ -0,0 +1,77 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import com.samskivert.util.ResultListener;
/**
* Extends the {@link RepositoryUnit} and integrates with a {@link ResultListener}.
*/
public abstract class RepositoryListenerUnit<T> extends RepositoryUnit
{
/**
* Creates a repository listener unit that will report its results to the supplied result
* listener.
*/
public RepositoryListenerUnit (ResultListener<T> listener)
{
super(String.valueOf(listener));
_listener = listener;
}
/**
* Creates a repository listener unit that will report its results to the supplied result
* listener and report the supplied name in {@link #toString}.
*/
public RepositoryListenerUnit (String name, ResultListener<T> listener)
{
super(name);
_listener = listener;
}
/**
* Called to perform our persistent action and generate our result.
*/
public abstract T invokePersistResult ()
throws Exception;
@Override // from RepositoryUnit
public void invokePersist ()
throws Exception
{
_result = invokePersistResult();
}
@Override // from RepositoryUnit
public void handleSuccess ()
{
_listener.requestCompleted(_result);
}
@Override // from RepositoryUnit
public void handleFailure (Exception pe)
{
_listener.requestFailed(pe);
}
protected ResultListener<T> _listener;
protected T _result;
}
@@ -0,0 +1,80 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
/**
* A specialized invoker unit that does one or more database operations and then sends the results
* back to the event processing thread. Override {@link #handleSuccess} to process your result on
* the event thread and {@link #handleFailure} to handle a failure on the event thread. For
* example:
*
* <pre>
* final int userId = 123;
* _invoker.postUnit(new RepositoryUnit("loadUser") {
* public void invokePersist () throws Exception {
* _user = _userRepo.loadUser(userId);
* }
* public void handleSuccess () {
* // do something with _user
* }
* public void handleFailure (Exception cause) {
* // report failure to load user
* }
* protected UserRecord _user;
* });
* </pre>
*/
public abstract class RepositoryUnit extends WriteOnlyUnit
{
/**
* Create a RepositoryUnit which will report the supplied name in {@link #toString}.
*/
public RepositoryUnit (String name)
{
super(name);
}
@Override // from WriteOnlyUnit
public boolean invoke ()
{
try {
invokePersist();
} catch (Exception pe) {
_error = pe;
}
return true;
}
@Override // from WriteOnlyUnit
public void handleResult ()
{
if (_error != null) {
handleFailure(_error);
} else {
handleSuccess();
}
}
/**
* Called if our persistent actions have succeeded, back on the non-invoker thread.
*/
public abstract void handleSuccess ();
}
@@ -0,0 +1,40 @@
package com.samskivert.jdbc;
/**
* A RepositoryUnit that returns a single result from its database operations and operates on that.
*/
public abstract class ResultUnit<T> extends RepositoryUnit
{
public ResultUnit (String name)
{
super(name);
}
/**
* Performs actions on the database and returns exciting data.
*/
public abstract T computeResult ()
throws Exception;
/**
* Operates on the result from <code>computeResult</code> back on the main thread, if
* <code>computeResult</code> succeeded.
*/
public abstract void handleResult (T result);
@Override
public void handleSuccess ()
{
handleResult(_result);
}
@Override
public void invokePersist ()
throws Exception
{
_result = computeResult();
}
protected T _result;
}
@@ -0,0 +1,408 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.*;
import com.samskivert.io.PersistenceException;
import com.samskivert.util.StringUtil;
import static com.samskivert.Log.log;
/**
* The simple repository should be used for a repository that only needs access to a single JDBC
* connection instance to perform its persistence services.
*/
public class SimpleRepository extends Repository
{
/** See {@link #setExecutePreCondition}. */
public static interface PreCondition
{
/** See {@link #setExecutePreCondition}. */
public boolean validate (String dbident, Operation<?> op);
}
/**
* Configures an operation that will be invoked prior to the execution of every database
* operation to validate whether some pre-condition is met. This mainly exists for systems that
* wish to ensure that all database operations take place on a particular thread (or not on a
* particular thread) as database operations are generally slow and blocking.
*/
public static void setExecutePreCondition (PreCondition condition)
{
_precond = condition;
}
/**
* Creates and initializes a simple repository which will access the database identified by the
* supplied database identifier.
*
* @param provider the connection provider which will be used to obtain our database
* connection.
* @param dbident the identifier of the database that will be accessed by this repository or
* null if the derived class will call {@link #configureDatabaseIdent} by hand later.
*/
public SimpleRepository (ConnectionProvider provider, String dbident)
{
super(provider);
if (dbident != null) {
configureDatabaseIdent(dbident);
}
}
/**
* This is called automatically if a dbident is provided at construct time, but a derived class
* can pass null to its constructor and then call this method itself later if it wishes to
* obtain its database identifier from an overridable method which could not otherwise be
* called at construct time.
*/
protected void configureDatabaseIdent (String dbident)
{
_dbident = dbident;
// give the repository a chance to do any schema migration before things get further
// underway
try {
executeUpdate(new Operation<Object>() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
migrateSchema(conn, liaison);
return null;
}
});
} catch (PersistenceException pe) {
log.warning("Failure migrating schema", "dbident", _dbident, pe);
}
}
/**
* Executes the supplied read-only operation. In the event of a transient failure, the
* repository will attempt to reestablish the database connection and try the operation again.
*
* @return whatever value is returned by the invoked operation.
*/
protected <V> V execute (Operation<V> op)
throws PersistenceException
{
return execute(op, true, true);
}
/**
* Executes the supplied read-write operation. In the event of a transient failure, the
* repository will attempt to reestablish the database connection and try the operation again.
*
* @return whatever value is returned by the invoked operation.
*/
protected <V> V executeUpdate (Operation<V> op)
throws PersistenceException
{
return execute(op, true, false);
}
/**
* Executes the supplied operation followed by a call to <code>commit()</code> on the
* connection unless a <code>PersistenceException</code> or runtime error occurs, in which case
* a call to <code>rollback()</code> is executed on the connection.
*
* @param retryOnTransientFailure if true and the operation fails due to a transient failure
* (like losing the connection to the database or deadlock detection), the connection to the
* database will be reestablished (if necessary) and the operation attempted once more.
* @param readOnly whether or not to request a read-only connection.
*
* @return whatever value is returned by the invoked operation.
*/
protected <V> V execute (Operation<V> op, boolean retryOnTransientFailure, boolean readOnly)
throws PersistenceException
{
Connection conn = null;
DatabaseLiaison liaison = null;
V rv = null;
boolean supportsTransactions = false;
boolean attemptedOperation = false;
Boolean oldAutoCommit = null;
// check our pre-condition
if (_precond != null && !_precond.validate(_dbident, op)) {
log.warning("Repository operation failed pre-condition check!", "dbident", _dbident,
"op", op, new Exception());
}
// obtain our database connection and associated goodies
conn = _provider.getConnection(_dbident, readOnly);
// make sure that no one else performs a database operation using the same connection until
// we're done
synchronized (conn) {
try {
liaison = LiaisonRegistry.getLiaison(conn);
// find out if we support transactions
DatabaseMetaData dmd = conn.getMetaData();
if (dmd != null) {
supportsTransactions = dmd.supportsTransactions();
}
// turn off auto-commit
if (supportsTransactions && conn.getAutoCommit()) {
oldAutoCommit = conn.getAutoCommit();
conn.setAutoCommit(false);
}
// let derived classes do any got-connection processing
gotConnection(conn);
// invoke the operation
attemptedOperation = true;
rv = op.invoke(conn, liaison);
// commit the transaction
if (supportsTransactions) {
conn.commit();
}
// return the operation result
return rv;
} catch (SQLException sqe) {
if (attemptedOperation) {
// back out our changes if something got hosed (but not if the hosage was a
// result of losing our connection)
try {
if (supportsTransactions && !conn.isClosed()) {
conn.rollback();
}
} catch (SQLException rbe) {
log.warning("Unable to roll back operation", "err", sqe, "rberr", rbe);
}
}
if (conn != null) {
// let the provider know that the connection failed
_provider.connectionFailed(_dbident, readOnly, conn, sqe);
// clear out the reference so that we don't release it later
conn = null;
}
if (!retryOnTransientFailure || liaison == null ||
!liaison.isTransientException(sqe)) {
String err = "Operation invocation failed";
throw new PersistenceException(err, sqe);
}
// the MySQL JDBC driver has the annoying habit of including the embedded exception
// stack trace in the message of their outer exception; if I want a fucking stack
// trace, I'll call printStackTrace() thanksverymuch
String msg = StringUtil.split("" + sqe, "\n")[0];
log.info("Transient failure executing operation, retrying", "error", msg);
} catch (PersistenceException pe) {
// back out our changes if something got hosed
try {
if (supportsTransactions && !conn.isClosed()) {
conn.rollback();
}
} catch (SQLException rbe) {
log.warning("Unable to roll back operation", "origerr", pe, "rberr", rbe);
}
throw pe;
} catch (RuntimeException rte) {
// back out our changes if something got hosed
try {
if (supportsTransactions && conn != null && !conn.isClosed()) {
conn.rollback();
}
} catch (SQLException rbe) {
log.warning("Unable to roll back operation", "origerr", rte, "rberr", rbe);
}
throw rte;
} finally {
if (conn != null) {
try {
// restore our auto-commit settings
if (oldAutoCommit != null && !conn.isClosed()) {
conn.setAutoCommit(oldAutoCommit);
}
} catch (SQLException sace) {
log.warning("Unable to restore auto-commit", "err", sace);
}
// release the database connection
_provider.releaseConnection(_dbident, readOnly, conn);
}
}
}
// we'll only fall through here if the above code failed due to a transient exception (the
// connection was closed for being idle, for example) and we've been asked to retry; so
// let's do so
return execute(op, false, readOnly);
}
/**
* Executes the supplied update query in this repository, returning the number of rows
* modified.
*/
protected int update (final String query)
throws PersistenceException
{
return executeUpdate(new Operation<Integer>() {
public Integer invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
Statement stmt = null;
try {
stmt = conn.createStatement();
return stmt.executeUpdate(query);
} finally {
JDBCUtil.close(stmt);
}
}
});
}
/**
* Executes the supplied update query in this repository, throwing an exception if the
* modification count is not equal to the specified count.
*/
protected void checkedUpdate (final String query, final int count)
throws PersistenceException
{
executeUpdate(new Operation<Object>() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
Statement stmt = null;
try {
stmt = conn.createStatement();
JDBCUtil.checkedUpdate(stmt, query, 1);
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
/**
* Executes the supplied update query in this repository, logging a warning if the modification
* count is not equal to the specified count.
*/
protected void warnedUpdate (final String query, final int count)
throws PersistenceException
{
executeUpdate(new Operation<Object>() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
Statement stmt = null;
try {
stmt = conn.createStatement();
JDBCUtil.warnedUpdate(stmt, query, 1);
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
/**
* Instructs MySQL to perform table maintenance on the specified table.
*
* @param action <code>analyze</code> recomputes the distribution of the keys for the specified
* table. This can help certain joins to be performed more efficiently. <code>optimize</code>
* instructs MySQL to coalesce fragmented records and reclaim space left by deleted records.
* This can improve a tables efficiency but can take a long time to run on large tables.
*/
protected void maintenance (final String action, final String table)
throws PersistenceException
{
executeUpdate(new Operation<Object>() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
Statement stmt = null;
try {
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(action + " table " + table);
while (rs.next()) {
String result = rs.getString("Msg_text");
if (result == null ||
(result.indexOf("up to date") == -1 && !result.equals("OK"))) {
log.info("Table maintenance [" + SimpleRepository.toString(rs) + "].");
}
}
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
/**
* Derived classes can override this method and perform any schema migration they might need
* (using the idempotent {@link JDBCUtil} schema migration methods). This is called during the
* repository's constructor and will thus take place before derived classes (like the {@link
* JORARepository} introspect on the schema to match it up to associated Java classes).
*/
protected void migrateSchema (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
}
/**
* Called when we fetch a connection from the provider. This gives derived classes an
* opportunity to configure whatever internals they might be using with the connection that was
* fetched.
*/
protected void gotConnection (Connection conn)
{
}
/**
* Converts a row of a result set to a string, prepending each column with the column name from
* the result set metadata.
*/
protected static String toString (ResultSet rs)
throws SQLException
{
ResultSetMetaData md = rs.getMetaData();
int ccount = md.getColumnCount();
StringBuilder buf = new StringBuilder();
for (int ii = 1; ii <= ccount; ii++) {
if (buf.length() > 0) {
buf.append(", ");
}
buf.append(md.getColumnName(ii)).append("=");
buf.append(rs.getObject(ii));
}
return buf.toString();
}
protected String _dbident;
protected static PreCondition _precond;
}
@@ -0,0 +1,295 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.io.IOException;
import java.sql.*;
import java.util.*;
import com.samskivert.io.PersistenceException;
import com.samskivert.util.ConfigUtil;
import com.samskivert.util.PropertiesUtil;
import com.samskivert.util.StringUtil;
import static com.samskivert.Log.log;
/**
* The static connection provider generates JDBC connections based on configuration information
* provided via a properties file. It does no connection pooling and always returns the same
* connection for a particular identifier (unless that connection need be closed because of a
* connection failure, in which case it opens a new one the next time the connection is requested).
*
* <p> The configuration properties file should contain the following information:
*
* <pre>
* IDENT.driver=[jdbc driver class]
* IDENT.url=[jdbc driver url]
* IDENT.username=[jdbc username]
* IDENT.password=[jdbc password]
*
* [...]
* </pre>
*
* Where <code>IDENT</code> is the database identifier for a particular database connection. When a
* particular database identifier is requested, the configuration information will be fetched from
* the properties.
*
* <p> Additionally, a default set of properties can be provided using the identifier
* <code>default</code>. Values not provided for a specific identifier will be sought from the
* defaults. For example:
*
* <pre>
* default.driver=[jdbc driver class]
* default.url=[jdbc driver class]
*
* IDENT1.username=[jdbc username]
* IDENT1.password=[jdbc password]
*
* IDENT2.username=[jdbc username]
* IDENT2.password=[jdbc password]
*
* [...]
* </pre>
*/
public class StaticConnectionProvider implements ConnectionProvider
{
/**
* Constructs a static connection provider which will load its configuration from a properties
* file accessible via the classpath of the running application and identified by the specified
* path.
*
* @param propPath the path (relative to the classpath) to the properties file that will be
* used for configuration information.
*
* @exception IOException thrown if an error occurs locating or loading the specified
* properties file.
*/
public StaticConnectionProvider (String propPath)
throws IOException
{
this(ConfigUtil.loadProperties(propPath));
}
/**
* Constructs a static connection provider which will fetch its configuration information from
* the specified properties object.
*
* @param props the configuration for this connection provider.
*/
public StaticConnectionProvider (Properties props)
{
_props = props;
}
// from ConnectionProvider
public String getURL (String ident)
{
Properties props = PropertiesUtil.getSubProperties(_props, ident, DEFAULTS_KEY);
return props.getProperty("url");
}
// documentation inherited
public Connection getConnection (String ident, boolean readOnly)
throws PersistenceException
{
String mapkey = ident + ":" + readOnly;
Mapping conmap = _idents.get(mapkey);
// open the connection if we haven't already
if (conmap == null) {
Properties props = PropertiesUtil.getSubProperties(_props, ident, DEFAULTS_KEY);
// get the JDBC configuration info
String err = "No driver class specified [ident=" + ident + "].";
String driver = requireProp(props, "driver", err);
err = "No driver URL specified [ident=" + ident + "].";
String url = requireProp(props, "url", err);
err = "No driver username specified [ident=" + ident + "].";
String username = requireProp(props, "username", err);
String password = props.getProperty("password", "");
String autoCommit = props.getProperty("autocommit");
// if this is a read-only connection, we cache connections by username+url+readOnly to
// avoid making more that one connection to a particular database server
String key = username + "@" + url + ":" + readOnly;
conmap = _keys.get(key);
if (conmap == null) {
log.debug("Creating " + key + " for " + ident + ".");
conmap = new Mapping();
conmap.key = key;
conmap.connection = openConnection(driver, url, username, password);
// if we were requested to configure auto-commit, then do so
if (autoCommit != null) {
try {
conmap.connection.setAutoCommit(Boolean.valueOf(autoCommit));
} catch (SQLException sqe) {
closeConnection(ident, conmap.connection);
err = "Failed to configure auto-commit [key=" + key +
", ident=" + ident + ", autoCommit=" + autoCommit + "].";
throw new PersistenceException(err, sqe);
}
}
// make the connection read-only to let the JDBC driver know that it can and should
// use the read-only mirror(s)
if (readOnly) {
try {
conmap.connection.setReadOnly(true);
} catch (SQLException sqe) {
closeConnection(ident, conmap.connection);
err = "Failed to make connection read-only [key=" + key +
", ident=" + ident + "].";
throw new PersistenceException(err, sqe);
}
}
_keys.put(key, conmap);
} else {
log.debug("Reusing " + key + " for " + ident + ".");
}
// cache the connection
conmap.idents.add(mapkey);
_idents.put(mapkey, conmap);
}
return conmap.connection;
}
// documentation inherited
public void releaseConnection (String ident, boolean readOnly, Connection conn)
{
// nothing to do here, all is well
}
// documentation inherited
public void connectionFailed (
String ident, boolean readOnly, Connection conn, SQLException error)
{
String mapkey = ident + ":" + readOnly;
Mapping conmap = _idents.get(mapkey);
if (conmap == null) {
log.warning("Unknown connection failed!?", "key", mapkey);
return;
}
// attempt to close the connection
closeConnection(ident, conmap.connection);
// clear it from our mapping tables
for (int ii = 0; ii < conmap.idents.size(); ii++) {
_idents.remove(conmap.idents.get(ii));
}
_keys.remove(conmap.key);
}
// from ConnectionProvider
public void shutdown ()
{
// close all of the connections
for (Map.Entry<String, Mapping> entry : _keys.entrySet()) {
Mapping conmap = entry.getValue();
try {
conmap.connection.close();
} catch (SQLException sqe) {
log.warning("Error shutting down connection", "key", entry.getKey(), "err", sqe);
}
}
// clear out our mapping tables
_keys.clear();
_idents.clear();
}
protected Connection openConnection (String driver, String url, String username, String passwd)
throws PersistenceException
{
// create an instance of the driver
Driver jdriver;
try {
jdriver = (Driver)Class.forName(driver).newInstance();
} catch (Exception e) {
String err = "Error loading driver [class=" + driver + "].";
throw new PersistenceException(err, e);
}
// create the connection
try {
Properties props = new Properties();
props.put("user", username);
props.put("password", passwd);
return jdriver.connect(url, props);
} catch (SQLException sqe) {
String err = "Error creating database connection [driver=" + driver + ", url=" + url +
", username=" + username + "].";
throw new PersistenceException(err, sqe);
}
}
protected void closeConnection (String ident, Connection conn)
{
try {
conn.close();
} catch (SQLException sqe) {
log.warning("Error closing failed connection", "ident", ident, "error", sqe);
}
}
protected static String requireProp (Properties props, String name, String errmsg)
throws PersistenceException
{
String value = props.getProperty(name);
if (StringUtil.isBlank(value)) {
errmsg = "Unable to get connection. " + errmsg; // augment the error message
throw new PersistenceException(errmsg);
}
return value;
}
/** Contains information on a particular connection to which any number of database identifiers
* can be mapped. */
protected static class Mapping
{
/** The combination of username and JDBC url that uniquely identifies our database
* connection. */
public String key;
/** The connection itself. */
public Connection connection;
/** The database identifiers that are mapped to this connection. */
public List<String> idents = new ArrayList<String>();
}
/** Our configuration in the form of a properties object. */
protected Properties _props;
/** A mapping from database identifier to connection records. */
protected HashMap<String,Mapping> _idents = new HashMap<String,Mapping>();
/** A mapping from connection key to connection records. */
protected HashMap<String,Mapping> _keys = new HashMap<String,Mapping>();
/** The key used as defaults for the database definitions. */
protected static final String DEFAULTS_KEY = "default";
}
@@ -0,0 +1,212 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import com.samskivert.io.PersistenceException;
import static com.samskivert.Log.log;
/**
* Used to note that transitionary code has been run to migrate persistent data. This is especially
* useful for data that one cannot examine to determine if it's been transitioned.
*/
public class TransitionRepository extends SimpleRepository
{
/** The identifier of this connection. */
public static final String TRANSITION_DB_IDENT = "transitiondb";
/**
* An interface for the transition.
*/
public static interface Transition
{
/**
* Do the transition.
*/
public void run () throws PersistenceException;
}
/**
* Construct a TransitionRepository for the server.
*/
public TransitionRepository (ConnectionProvider conprov)
throws PersistenceException
{
super(conprov, TRANSITION_DB_IDENT);
}
/**
* Perform a transition if it has not already been applied, and record that it was applied.
*/
public void transition (Class<?> clazz, String name, Transition trans)
throws PersistenceException
{
if (!isTransitionApplied(clazz, name) && noteTransition(clazz, name)) {
try {
trans.run();
} catch (PersistenceException e) {
try {
clearTransition(clazz, name);
} catch (PersistenceException pe) {
log.warning("Failed to clear failed transition", "class", clazz, "name", name,
pe);
}
throw e;
} catch (RuntimeException rte) {
try {
clearTransition(clazz, name);
} catch (PersistenceException pe) {
log.warning("Failed to clear failed transition", "class", clazz, "name", name,
pe);
}
throw rte;
}
}
}
/**
* Returns whether the specified name transition been applied.
*/
public boolean isTransitionApplied (Class<?> clazz, final String name)
throws PersistenceException
{
final String cname = clazz.getName();
return execute(new Operation<Boolean>() {
public Boolean invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(
" select " + liaison.columnSQL("NAME") +
" from " + liaison.tableSQL("TRANSITIONS") +
" where " + liaison.columnSQL("CLASS") + "=?" +
" and " + liaison.columnSQL("NAME") + "=?");
stmt.setString(1, cname);
stmt.setString(2, name);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return true;
}
} finally {
JDBCUtil.close(stmt);
}
return false;
}
});
}
/**
* Note in the database that a particular transition has been applied.
*
* @return true if the transition was noted, false if it could not be noted because another
* process noted it first.
*/
public boolean noteTransition (Class<?> clazz, final String name)
throws PersistenceException
{
final String cname = clazz.getName();
return executeUpdate(new Operation<Boolean>() {
public Boolean invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(
"insert into " + liaison.tableSQL("TRANSITIONS") + " (" +
liaison.columnSQL("CLASS") + ", " + liaison.columnSQL("NAME") +
", " + liaison.columnSQL("APPLIED") +
") values (?, ?, ?)");
stmt.setString(1, cname);
stmt.setString(2, name);
stmt.setTimestamp(3, new Timestamp(System.currentTimeMillis()));
JDBCUtil.checkedUpdate(stmt, 1);
return true;
} catch (SQLException sqe) {
if (liaison.isDuplicateRowException(sqe)) {
return false;
} else {
throw sqe;
}
} finally {
JDBCUtil.close(stmt);
}
}
});
}
/**
* Clear the transition.
*/
public void clearTransition (Class<?> clazz, final String name)
throws PersistenceException
{
final String cname = clazz.getName();
executeUpdate(new Operation<Void>() {
public Void invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(
" delete from " + liaison.tableSQL("TRANSITIONS") +
" where " + liaison.columnSQL("CLASS") + "=? " +
" and " + liaison.columnSQL("NAME") + "=?");
stmt.setString(1, cname);
stmt.setString(2, name);
stmt.executeUpdate(); // we don't care if it worked or not
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
@Override
protected void migrateSchema (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
liaison.createTableIfMissing(
conn,
"TRANSITIONS",
new String[] { "CLASS", "NAME", "APPLIED" },
new ColumnDefinition[] {
new ColumnDefinition("VARCHAR(200)", true, false, null),
new ColumnDefinition("VARCHAR(50)", true, false, null),
new ColumnDefinition("TIMESTAMP")
},
null,
new String[] { "CLASS", "NAME" });
}
}
@@ -0,0 +1,84 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import com.samskivert.util.Invoker;
import static com.samskivert.Log.log;
/**
* Extends {@link com.samskivert.util.Invoker.Unit} and specializes it for writing to a database
* repository.
*/
public abstract class WriteOnlyUnit extends Invoker.Unit
{
/**
* Creates a unit which will report the supplied name in {@link #toString} and in the event of
* failure.
*/
public WriteOnlyUnit (String name)
{
super(name);
}
@Override // from abstract Invoker.Unit
public boolean invoke ()
{
try {
invokePersist();
return false;
} catch (Exception pe) {
_error = pe;
return true;
}
}
@Override // from Invoker.Unit
public void handleResult ()
{
handleFailure(_error);
}
/**
* Called to perform our persistent actions.
*/
public abstract void invokePersist ()
throws Exception;
/**
* Called if our persistent actions failed, back on the non-invoker thread. The default
* implementation logs an error message and a stack trace.
*/
public void handleFailure (Exception e)
{
log.warning(getFailureMessage(), e);
}
/**
* Returns the error message to be logged if {@link #invokePersist} throws an exception.
*/
protected String getFailureMessage ()
{
return this + " failed.";
}
protected Exception _error;
}
@@ -0,0 +1,233 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc.jora;
import java.util.*;
import java.sql.*;
import static com.samskivert.Log.log;
/**
* Cursor is used for successive access to records fetched by SELECT statement.
* As far as records can be retrived from several derived tables (polymorphic
* form of select), this class can issue several requests to database. Cursor
* also provides methods for updating/deleting current record.
*/
public class Cursor<V>
{
/**
* A cursor is initially positioned before its first row; the first call to
* next makes the first row the current row; the second call makes the
* second row the current row, etc.
*
* <P> If an input stream from the previous row is open, it is implicitly
* closed. The ResultSet's warning chain is cleared when a new row is read.
*
* @return object constructed from fetched record or null if there are no
* more rows
*/
public V next ()
throws SQLException
{
// if we closed everything up after the last call to next(),
// table will be null here and we should bail immediately
if (_table == null) {
return null;
}
if (_result == null) {
if (_qbeObject != null) {
PreparedStatement qbeStmt = _conn.prepareStatement(_query);
_table.bindQueryVariables(qbeStmt, _qbeObject, _qbeMask);
_result = qbeStmt.executeQuery();
_stmt = qbeStmt;
} else {
if (_stmt == null) {
_stmt = _conn.createStatement();
}
_result = _stmt.executeQuery(_query);
}
}
if (_result.next()) {
return _currObject = _table.load(_result);
}
_result.close();
_result = null;
_currObject = null;
_table = null;
if (_stmt != null) {
_stmt.close();
}
return null;
}
/**
* Returns the first element matched by this cursor or null if no elements
* were matched. Checks to ensure that no subsequent elements were matched
* by the query, logs a warning if there were spurious additional matches.
*/
public V get ()
throws SQLException
{
V result = next();
if (result != null) {
int spurious = 0;
while (next() != null) {
spurious++;
}
if (spurious > 0) {
log.warning("Cursor.get() quietly tossed " + spurious + " spurious additional " +
"records.", "query", _query);
}
}
return result;
}
/**
* Update current record pointed by cursor. This method can be called only
* after next() method, which returns non-null object. This objects is used
* to update current record fields.
*
* <P> If you are going to update or delete selected records, you should
* add "for update" clause to select statement. So parameter of
* <CODE>jora.Table.select()</CODE> statement should contain "for update"
* clause: <CODE>record.table.Select("where name='xyz' for
* update");</CODE><P>
*
* <I><B>Attention!</I></B> Not all database drivers support update
* operation with cursor. This method will not work with such database
* drivers.
*/
public void update ()
throws SQLException
{
if (_currObject == null) {
throw new IllegalStateException("No current object");
}
_table.updateVariables(_result, _currObject);
}
/**
* Delete current record pointed by cursor. This method can be called only
* after next() method, which returns non-null object.
*
* <P> If you are going to update or delete selected records, you should
* add "for update" clause to select statement. So parameter of
* <CODE>jora.Table.select()</CODE> statement should contain "for update"
* clause: <CODE>record.table.Select("where name='xyz' for
* update");</CODE><P>
*
* <I><B>Attention!</I></B> Not all database drivers support delete
* operation with cursor. This method will not work with such database
* drivers.
*/
public void delete ()
throws SQLException
{
if (_currObject == null) {
throw new IllegalStateException("No current object");
}
_result.deleteRow();
}
/**
* Close the Cursor, even if we haven't read all the possible objects.
*/
public void close ()
throws SQLException
{
if (_result != null) {
_result.close();
_result = null;
}
if (_stmt != null) {
_stmt.close();
_stmt = null;
}
}
/**
* Extracts no more than <I>maxElements</I> records from database and store
* them into array. It is possible to extract rest records by successive
* next() or toArray() calls. Selected objects should have now components
* of InputStream, Blob or Clob type, because their data will be not
* available after fetching next record.
*
* @param maxElements limitation for result array size (and also for number
* of fetched records)
* @return List with objects constructed from fetched records.
*/
public ArrayList<V> toArrayList (int maxElements)
throws SQLException
{
ArrayList<V> al = new ArrayList<V>(Math.min(maxElements, 100));
V o;
while (--maxElements >= 0 && (o = next()) != null) {
al.add(o);
}
return al;
}
/**
* Store all objects returned by SELECT query into a list of Object.
* Selected objects should have now components of InputStream, Blob or Clob
* type, because their data will be not available after fetching next
* record.
*
* @return Array with objects constructed from fetched records.
*/
public ArrayList<V> toArrayList ()
throws SQLException
{
return toArrayList(Integer.MAX_VALUE);
}
protected Cursor (Table<V> table, Connection conn, String query)
{
_table = table;
_conn = conn;
_query = query;
}
protected Cursor (Table<V> table, Connection conn, V obj,
FieldMask mask, boolean like)
{
_table = table;
_conn = conn;
_like = like;
_qbeObject = obj;
_qbeMask = mask;
_query = table.buildQueryList(obj, mask, like);
_stmt = null;
}
protected Table<V> _table;
protected Connection _conn;
protected ResultSet _result;
protected String _query;
protected Statement _stmt;
protected V _currObject, _qbeObject;
protected FieldMask _qbeMask;
protected boolean _like;
}
@@ -0,0 +1,34 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc.jora;
/** This error is raised when error is happened during data transfer
* between program and database server (for example IOException was thrown
* while operation with InputStream field)
*/
public class DataTransferError extends java.lang.Error {
DataTransferError() {
super("Database data transfer error");
}
DataTransferError(Exception ex) {
super(ex);
}
}
@@ -0,0 +1,435 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc.jora;
import java.sql.*;
import java.math.*;
import java.lang.reflect.*;
class FieldDescriptor
{
protected FieldDescriptor (Field field, String name)
{
this.name = name;
this.field = field;
this.scale = -1;
}
protected final boolean isAtomic ()
{
return inType < tClosure;
}
protected final boolean isCompound ()
{
return inType >= tCompound;
}
protected final boolean isBuiltin ()
{
return inType <= t_boolean;
}
protected final boolean bindVariable (
PreparedStatement pstmt, Object obj, int column)
throws SQLException
{
try {
switch (outType) {
case t_byte:
pstmt.setByte(column, field.getByte(obj));
break;
case t_short:
pstmt.setShort(column, field.getShort(obj));
break;
case t_int:
pstmt.setInt(column, field.getInt(obj));
break;
case t_long:
pstmt.setLong(column, field.getLong(obj));
break;
case t_float:
pstmt.setFloat(column, field.getFloat(obj));
break;
case t_double:
pstmt.setDouble(column, field.getDouble(obj));
break;
case t_boolean:
pstmt.setBoolean(column, field.getBoolean(obj));
break;
case tByte:
pstmt.setByte(column, ((Byte)field.get(obj)).byteValue());
break;
case tShort:
pstmt.setShort(column, ((Short)field.get(obj)).shortValue());
break;
case tInteger:
pstmt.setInt(column, ((Integer)field.get(obj)).intValue());
break;
case tLong:
pstmt.setLong(column, ((Long)field.get(obj)).longValue());
break;
case tFloat:
pstmt.setFloat(column, ((Float)field.get(obj)).floatValue());
break;
case tDouble:
pstmt.setDouble(column,((Double)field.get(obj)).doubleValue());
break;
case tBoolean:
pstmt.setBoolean(column, ((Boolean)field.get(obj)).booleanValue());
break;
case tDecimal:
pstmt.setBigDecimal(column, (BigDecimal)field.get(obj));
break;
case tString:
pstmt.setString(column, (String)field.get(obj));
break;
case tBytes:
pstmt.setBytes(column, (byte[])field.get(obj));
break;
case tDate:
pstmt.setDate(column, (java.sql.Date)field.get(obj));
break;
case tTime:
pstmt.setTime(column, (java.sql.Time)field.get(obj));
break;
case tTimestamp:
pstmt.setTimestamp(column, (java.sql.Timestamp)field.get(obj));
break;
case tStream:
java.io.InputStream in = (java.io.InputStream)field.get(obj);
pstmt.setBinaryStream(column, in, in.available());
break;
case tBlob:
pstmt.setBlob(column, (Blob)field.get(obj));
break;
case tClob:
pstmt.setClob(column, (Clob)field.get(obj));
break;
case tAsString:
pstmt.setString(column, field.get(obj).toString());
break;
case tClosure:
// There is no reason to use piped streams because
// we need to pass total number of bytes to JDBC driver
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
java.io.ObjectOutputStream clu = new java.io.ObjectOutputStream(out);
clu.writeObject(field.get(obj));
clu.close();
pstmt.setBytes(column, out.toByteArray());
break;
default:
return false;
}
} catch(SQLException ex) {
if (outType != tClosure && outType != tAsString) {
outType = tAsString;
return bindVariable(pstmt, obj, column);
} else {
throw ex;
}
} catch(IllegalAccessException ex) {
ex.printStackTrace();
throw new IllegalAccessError();
} catch(java.io.IOException ex) {
throw new DataTransferError(ex);
}
return true;
}
protected final boolean updateVariable (ResultSet result, Object obj, int column)
throws SQLException
{
try {
switch (outType) {
case t_byte:
result.updateByte(column, field.getByte(obj));
break;
case t_short:
result.updateShort(column, field.getShort(obj));
break;
case t_int:
result.updateInt(column, field.getInt(obj));
break;
case t_long:
result.updateLong(column, field.getLong(obj));
break;
case t_float:
result.updateFloat(column, field.getFloat(obj));
break;
case t_double:
result.updateDouble(column, field.getDouble(obj));
break;
case t_boolean:
result.updateBoolean(column, field.getBoolean(obj));
break;
case tByte:
result.updateByte(column, ((Byte)field.get(obj)).byteValue());
break;
case tShort:
result.updateShort(column, ((Short)field.get(obj)).shortValue());
break;
case tInteger:
result.updateInt(column, ((Integer)field.get(obj)).intValue());
break;
case tLong:
result.updateLong(column, ((Long)field.get(obj)).longValue());
break;
case tFloat:
result.updateFloat(column, ((Float)field.get(obj)).floatValue());
break;
case tDouble:
result.updateDouble(column, ((Double)field.get(obj)).doubleValue());
break;
case tBoolean:
result.updateBoolean(column, ((Boolean)field.get(obj)).booleanValue());
break;
case tDecimal:
result.updateBigDecimal(column, (BigDecimal)field.get(obj));
break;
case tString:
result.updateString(column, (String)field.get(obj));
break;
case tBytes:
result.updateBytes(column, (byte[])field.get(obj));
break;
case tDate:
result.updateDate(column, (java.sql.Date)field.get(obj));
break;
case tTime:
result.updateTime(column, (java.sql.Time)field.get(obj));
break;
case tTimestamp:
result.updateTimestamp(column, (java.sql.Timestamp)field.get(obj));
break;
case tStream:
java.io.InputStream in = (java.io.InputStream)field.get(obj);
result.updateBinaryStream(column, in, in.available());
break;
case tBlob:
Blob blob = (Blob)field.get(obj);
result.updateBinaryStream(column, blob.getBinaryStream(), (int)blob.length());
break;
case tClob:
Clob clob = (Clob)field.get(obj);
result.updateCharacterStream(column, clob.getCharacterStream(), (int)clob.length());
break;
case tAsString:
result.updateString(column, field.get(obj).toString());
break;
case tClosure:
// There is no reason to use piped streams because
// we need to pass total number of bytes to JDBC driver
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
java.io.ObjectOutputStream clu = new java.io.ObjectOutputStream(out);
clu.writeObject(field.get(obj));
clu.close();
result.updateBytes(column, out.toByteArray());
break;
default:
return false;
}
} catch(SQLException ex) {
if (outType != tClosure && outType != tAsString) {
outType = tAsString;
return updateVariable(result, obj, column);
} else {
throw ex;
}
} catch(IllegalAccessException ex) {
ex.printStackTrace();
throw new IllegalAccessError();
} catch(java.io.IOException ex) {
throw new DataTransferError(ex);
}
return true;
}
protected final boolean loadVariable (
ResultSet result, Object obj, int column)
throws SQLException, IllegalAccessException
{
switch (inType) {
case t_byte:
field.setByte(obj, result.getByte(column));
break;
case t_short:
field.setShort(obj, result.getShort(column));
break;
case t_int:
field.setInt(obj, result.getInt(column));
break;
case t_long:
field.setLong(obj, result.getLong(column));
break;
case t_float:
field.setFloat(obj, result.getFloat(column));
break;
case t_double:
field.setDouble(obj, result.getDouble(column));
break;
case t_boolean:
field.setBoolean(obj, result.getBoolean(column));
break;
case tByte:
byte b = result.getByte(column);
field.set(obj, result.wasNull() ? null : Byte.valueOf(b));
break;
case tShort:
short s = result.getShort(column);
field.set(obj, result.wasNull() ? null : Short.valueOf(s));
break;
case tInteger:
int i = result.getInt(column);
field.set(obj, result.wasNull() ? null : Integer.valueOf(i));
break;
case tLong:
long l = result.getLong(column);
field.set(obj, result.wasNull() ? null : Long.valueOf(l));
break;
case tFloat:
float f = result.getFloat(column);
field.set(obj, result.wasNull() ? null : Float.valueOf(f));
field.setFloat(obj, result.getFloat(column));
break;
case tDouble:
double d = result.getDouble(column);
field.set(obj, result.wasNull() ? null : Double.valueOf(d));
break;
case tBoolean:
boolean bl = result.getBoolean(column);
field.set(obj, result.wasNull() ? null : Boolean.valueOf(bl));
break;
case tDecimal:
field.set(obj, result.getBigDecimal(column));
break;
case tString:
field.set(obj, result.getString(column));
break;
case tBytes:
field.set(obj, result.getBytes(column));
break;
case tDate:
field.set(obj, result.getDate(column));
break;
case tTime:
field.set(obj, result.getTime(column));
break;
case tTimestamp:
field.set(obj, result.getTimestamp(column));
break;
case tStream:
field.set(obj, result.getBinaryStream(column));
break;
case tBlob:
field.set(obj, result.getBlob(column));
break;
case tClob:
field.set(obj, result.getClob(column));
break;
case tClosure:
try {
java.io.InputStream input = result.getBinaryStream(column);
java.io.ObjectInputStream in = new java.io.ObjectInputStream(input);
field.set(obj, in.readObject());
in.close();
} catch(ClassNotFoundException ex) {
throw new DataTransferError(ex);
} catch(java.io.IOException ex) {
throw new DataTransferError(ex);
}
break;
default:
return false;
}
return true;
}
protected int inType; // type tag for field input (see constants below)
protected int outType; // type tag for field output
protected int scale; // scale for tDecimal type,
protected String name; // full (compound) name of component
protected Field field; // field info from java.lang.reflect
protected Constructor<?> constructor; // constructor of object component
protected static final int t_byte = 0;
protected static final int t_short = 1;
protected static final int t_int = 2;
protected static final int t_long = 3;
protected static final int t_float = 4;
protected static final int t_double = 5;
protected static final int t_boolean = 6;
protected static final int tByte = 7;
protected static final int tShort = 8;
protected static final int tInteger = 9;
protected static final int tLong = 10;
protected static final int tFloat = 11;
protected static final int tDouble = 12;
protected static final int tBoolean = 13;
protected static final int tDecimal = 14;
protected static final int tString = 15;
protected static final int tBytes = 16;
protected static final int tDate = 17;
protected static final int tTime = 18;
protected static final int tTimestamp = 19;
protected static final int tStream = 20;
protected static final int tBlob = 21;
protected static final int tClob = 22;
protected static final int tAsString = 23;
protected static final int tClosure = 24;
protected static final int tCompound = 25;
protected static final int[] sqlTypeMapping = {
Types.INTEGER, // t_byte
Types.INTEGER, // t_short
Types.INTEGER, // t_int
Types.BIGINT, // t_long
Types.FLOAT, // t_float
Types.DOUBLE, // t_double
Types.BIT, // t_boolean
Types.INTEGER, // tByte
Types.INTEGER, // tShort
Types.INTEGER, // tInteger
Types.BIGINT, // tLong
Types.FLOAT, // tFloat
Types.DOUBLE, // tDouble
Types.BIT, // tBoolean
Types.NUMERIC, // tDecimal
Types.VARCHAR, // tString
Types.VARBINARY,// tBytes
Types.DATE, // tDate
Types.TIME, // tTime
Types.TIMESTAMP, // tTimestamp
Types.LONGVARBINARY, // tStream
Types.LONGVARBINARY, // tBlob
Types.LONGVARCHAR, // tClob
Types.VARCHAR, // tAsString
Types.LONGVARBINARY // tClosure
};
}
@@ -0,0 +1,188 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc.jora;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Provides support for doing partial updates to objects in a JORA table.
* A field mask can be obtained for a particular table, fields marked as
* modified and the object subsequently updated in a fairly
* straightforward manner:
*
* <pre>
* // updating parts of a table that contains User objects
* User user = // load user object from table
* FieldMask mask = table.getFieldMask();
* user.firstName = newFirstName;
* mask.setModified("firstName");
* user.lastName = newLastName;
* mask.setModified("lastName");
* table.update(user, mask);
* </pre>
*/
public class FieldMask
implements Cloneable
{
/**
* Creates a field mask for a {@link Table} that uses the supplied field descriptors.
*/
public FieldMask (FieldDescriptor[] descrips)
{
this(toNames(descrips));
}
/**
* Creates a field mask using the supplied field names.
*/
public FieldMask (String[] names)
{
for (int ii = 0; ii < names.length; ii++) {
_descripMap.put(names[ii], ii);
}
_modified = new boolean[names.length];
}
/**
* Returns true if any of the fields in this mask are modified.
*/
public final boolean isModified ()
{
int mcount = _modified.length;
for (int ii = 0; ii < mcount; ii++) {
if (_modified[ii]) {
return true;
}
}
return false;
}
/**
* Returns true if the field with the specified index is modified.
*/
public final boolean isModified (int index)
{
return _modified[index];
}
/**
* Returns true if the field with the specified name is modifed.
*/
public final boolean isModified (String fieldName)
{
Integer index = _descripMap.get(fieldName);
if (index == null) {
throw new IllegalArgumentException("Field not in mask: " + fieldName);
}
return _modified[index.intValue()];
}
/**
* Returns true only if the set of modified fields is a subset of the
* fields specified.
*/
public final boolean onlySubsetModified (Set<String> fieldSet)
{
for (String field : _descripMap.keySet()) {
if (isModified(field) && (!fieldSet.contains(field))) {
return false;
}
}
return true;
}
/**
* Marks the specified field as modified.
*/
public void setModified (String fieldName)
{
Integer index = _descripMap.get(fieldName);
if (index == null) {
throw new IllegalArgumentException("Field not in mask: " + fieldName);
}
_modified[index.intValue()] = true;
}
/**
* Clears out the modification state of the fields in this mask.
*/
public void clear ()
{
Arrays.fill(_modified, false);
}
/**
* Creates a copy of this field mask, with all fields set to
* not-modified.
*/
@Override
public FieldMask clone ()
{
try {
FieldMask mask = (FieldMask)super.clone();
mask._modified = new boolean[_modified.length];
return mask;
} catch (CloneNotSupportedException cnse) {
throw new AssertionError(cnse); // won't happen; we're Cloneable
}
}
@Override
public String toString ()
{
// return a list of the modified fields
StringBuilder buf = new StringBuilder("FieldMask [modified={");
boolean added = false;
for (Map.Entry<String,Integer> entry : _descripMap.entrySet()) {
if (_modified[entry.getValue().intValue()]) {
if (added) {
buf.append(", ");
} else {
added = true;
}
buf.append(entry.getKey());
}
}
buf.append("}]");
return buf.toString();
}
protected static String[] toNames (FieldDescriptor[] descrips)
{
// create a mapping from field name to descriptor index
int dcount = (descrips == null ? 0 : descrips.length);
String[] names = new String[dcount];
for (int ii = 0; ii < dcount; ii++) {
names[ii] = descrips[ii].field.getName();
}
return names;
}
/** Modified flags for each field of an object in this table. */
protected boolean[] _modified;
/** A mapping from field names to field descriptor index. */
protected Map<String, Integer> _descripMap = new HashMap<String, Integer>();
}
@@ -0,0 +1,29 @@
What have we here?
------------------
This is a hacked version of JORA, a library for automatically mapping
RDBMS database rows to Java objects and vice versa. It's simple, it uses
reflection and doesn't require any classfile post-processing or external
configuration information.
"That's all great but why is it hacked?" wonders the astute reader. Well,
I couldn't quite cope with the error handling paradigm that it used (catch
all SQL exceptions internally and pass them to an application-wide error
handler method) and a few seconds thought on the matter led me to believe
that there is not a simple way to allow a choice of error handling
paradigms (the methods have to throw SQLException or not). So I took the
easy way out and forked the codebase.
The code is pretty stable, so I don't expect to be merging in updates with
any frequency. I promise to be a good monkey and submit any useful
modifications that I make back to the original maintainer. It's a good
thing that the license was unrestrictive enough to allow me to do this,
otherwise I'd probably be stuck reinventing the wheel or using something
that does less of what I want. Three cheers for free software.
The original version can be found here: http://www.ispras.ru/~knizhnik/
(or at least it could when I wrote this README).
- mdb (2/12/2001)
$Id: README,v 1.2 2001/08/11 22:53:13 mdb Exp $
@@ -0,0 +1,919 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc.jora;
import java.io.Serializable;
import java.util.*;
import java.sql.*;
import java.lang.reflect.*;
import com.samskivert.util.StringUtil;
/**
* Used to establish mapping between corteges of database tables and Java
* classes. This class is responsible for constructing SQL statements for
* extracting, updating and deleting records of the database table.
*/
public class Table<T>
{
/**
* Constructor for table object. Make association between Java class and
* database table.
*
* @param clazz the class that represents a row entry.
* @param tableName name of database table mapped on this Java class
* @param key table's primary key. This parameter is used in UPDATE/DELETE
* operations to locate record in the table.
* @param mixedCaseConvert whether or not to convert mixed case field
* names into underscore separated uppercase column names.
*/
public Table (Class<T> clazz, String tableName, String key,
boolean mixedCaseConvert)
{
String[] keys = {key};
init(clazz, tableName, keys, mixedCaseConvert);
}
/**
* Constructor for table object. Make association between Java class and
* database table.
*
* @param clazz the class that represents a row entry.
* @param tableName name of database table mapped on this Java class
* @param key table's primary key. This parameter is used in UPDATE/DELETE
* operations to locate record in the table.
*/
public Table (Class<T> clazz, String tableName, String key) {
String[] keys = {key};
init(clazz, tableName, keys, false);
}
/**
* Constructor for table object. Make association between Java class and
* database table.
*
* @param clazz the class that represents a row entry.
* @param tableName name of database table mapped on this Java class
* @param keys table primary keys. This parameter is used in UPDATE/DELETE
* operations to locate record in the table.
*/
public Table (Class<T> clazz, String tableName, String[] keys)
{
init(clazz, tableName, keys, false);
}
/**
* Constructor for table object. Make association between Java class and
* database table.
*
* @param clazz the class that represents a row entry.
* @param tableName name of database table mapped on this Java class
* @param keys table primary keys. This parameter is used in UPDATE/DELETE
* operations to locate record in the table.
* @param mixedCaseConvert whether or not to convert mixed case field
* names into underscore separated uppercase column names.
*/
public Table (Class<T> clazz, String tableName, String[] keys,
boolean mixedCaseConvert)
{
init(clazz, tableName, keys, mixedCaseConvert);
}
/**
* Returns the SQL name of the table on which we operate.
*/
public String getName ()
{
return name;
}
/**
* Select records from database table according to search condition
*
* @param condition valid SQL condition expression started with WHERE or
* empty string if all records should be fetched.
*/
public final Cursor<T> select (Connection conn, String condition)
{
String query = "select " + listOfFields + " from " + name +
" " + condition;
return new Cursor<T>(this, conn, query);
}
/**
* Select records from database table according to search condition
* including the specified (comma separated) extra tables into the SELECT
* clause to facilitate a join in determining the key.
*
* @param tables the (comma separated) names of extra tables to include in
* the SELECT clause.
* @param condition valid SQL condition expression started with WHERE.
*/
public final Cursor<T> select (Connection conn, String tables,
String condition)
{
String query = "select " + qualifiedListOfFields +
" from " + name + "," + tables + " " + condition;
return new Cursor<T>(this, conn, query);
}
/**
* Select records from database table according to search condition
* including the specified (comma separated) extra tables into the SELECT
* clause to facilitate a join in determining the key. To facilitate
* situations where data from multiple tables is being combined into a
* single object, the fields will not be qualified with the primary table
* name.
*
* @param tables the (comma separated) names of extra tables to include in
* the SELECT clause.
* @param condition valid SQL condition expression started with WHERE.
*/
public final Cursor<T> join (Connection conn, String tables,
String condition)
{
String query = "select " + listOfFields +
" from " + name + "," + tables + " " + condition;
return new Cursor<T>(this, conn, query);
}
/**
* Like {@link #join} but does a straight join with the specified table.
*/
public final Cursor<T> straightJoin (Connection conn, String table,
String condition)
{
String query = "select " + listOfFields +
" from " + name + " straight_join " + table + " " + condition;
return new Cursor<T>(this, conn, query);
}
/**
* Select records from database table using <I>obj</I> object as template.
*
* @param obj example object for search: selected objects should match all
* non-null fields.
*/
public final Cursor<T> queryByExample (Connection conn, T obj)
{
return new Cursor<T>(this, conn, obj, null, false);
}
/**
* Select records from database table using <I>obj</I> object as template
* for selection.
*
* @param obj example object for search.
* @param mask field mask indicating which fields in the example object
* should be used when building the query.
*/
public final Cursor<T> queryByExample (Connection conn, T obj,
FieldMask mask)
{
return new Cursor<T>(this, conn, obj, mask, false);
}
/**
* The same as the queryByExample, but string fields for the obj are
* matched using 'like' instead of equals, which allows you to send % in to
* do matching.
*/
public final Cursor<T> queryByLikeExample (Connection conn, T obj)
{
return new Cursor<T>(this, conn, obj, null, true);
}
/**
* The same as the queryByExample, but string fields for the obj are
* matched using 'like' instead of equals, which allows you to send % in to
* do matching.
*/
public final Cursor<T> queryByLikeExample (Connection conn, T obj,
FieldMask mask)
{
return new Cursor<T>(this, conn, obj, mask, true);
}
/**
* Insert new record in the table. Values of inserted record fields are
* taken from specifed object.
*
* @param obj object specifing values of inserted record fields
*/
public synchronized void insert (Connection conn, T obj)
throws SQLException
{
StringBuilder sql = new StringBuilder(
"insert into " + name + " (" + listOfFields + ") values (?");
for (int i = 1; i < nColumns; i++) {
sql.append(",?");
}
sql.append(")");
PreparedStatement insertStmt = conn.prepareStatement(sql.toString());
bindUpdateVariables(insertStmt, obj, null);
insertStmt.executeUpdate();
insertStmt.close();
}
/**
* Insert several new records in the table. Values of inserted records
* fields are taken from objects of specified array.
*
* @param objects array with objects specifing values of inserted record
* fields
*/
public synchronized void insert (Connection conn, T[] objects)
throws SQLException
{
StringBuilder sql = new StringBuilder(
"insert into " + name + " (" + listOfFields + ") values (?");
for (int i = 1; i < nColumns; i++) {
sql.append(",?");
}
sql.append(")");
PreparedStatement insertStmt = conn.prepareStatement(sql.toString());
for (int i = 0; i < objects.length; i++) {
bindUpdateVariables(insertStmt, objects[i], null);
insertStmt.addBatch();
}
insertStmt.executeBatch();
insertStmt.close();
}
/**
* Returns a field mask that can be configured and used to update subsets
* of entire objects via calls to {@link #update(Connection,Object,FieldMask)}.
*/
public FieldMask getFieldMask ()
{
return fMask.clone();
}
/**
* Update record in the table using table's primary key to locate record in
* the table and values of fields of specified object <I>obj</I> to alter
* record fields.
*
* @param obj object specifing value of primary key and new values of
* updated record fields
*
* @return number of objects actually updated
*/
public int update (Connection conn, T obj)
throws SQLException
{
return update(conn, obj, null);
}
/**
* Update record in the table using table's primary key to locate record in
* the table and values of fields of specified object <I>obj</I> to alter
* record fields. Only the fields marked as modified in the supplied field
* mask will be updated in the database.
*
* @param obj object specifing value of primary key and new values of
* updated record fields
* @param mask a {@link FieldMask} instance configured to indicate which of
* the object's fields are modified and should be written to the database.
*
* @return number of objects actually updated
*/
public synchronized int update (Connection conn, T obj, FieldMask mask)
throws SQLException
{
int nUpdated = 0;
String sql = "update " + name + " set " +
(mask != null ? buildListOfAssignments(mask) : listOfAssignments) +
buildUpdateWhere();
PreparedStatement ustmt = conn.prepareStatement(sql);
int column = bindUpdateVariables(ustmt, obj, mask);
for (int i = 0; i < primaryKeys.length; i++) {
int fidx = primaryKeyIndices[i];
fields[fidx].bindVariable(ustmt, obj, column+i+1);
}
nUpdated = ustmt.executeUpdate();
ustmt.close();
return nUpdated;
}
/**
* Update set of records in the table using table's primary key to locate
* record in the table and values of fields of objects from sepecifed array
* <I>objects</I> to alter record fields.
*
* @param objects array of objects specifing primiray keys and and new
* values of updated record fields
*
* @return number of objects actually updated
*/
public synchronized int update (Connection conn, T[] objects)
throws SQLException
{
if (primaryKeys == null) {
throw new IllegalStateException(
"No primary key for table " + name + ".");
}
int nUpdated = 0;
String sql = "update " + name + " set " + listOfAssignments +
buildUpdateWhere();
PreparedStatement updateStmt = conn.prepareStatement(sql);
for (int i = 0; i < objects.length; i++) {
int column = bindUpdateVariables(updateStmt, objects[i], null);
for (int j = 0; j < primaryKeys.length; j++) {
int fidx = primaryKeyIndices[j];
fields[fidx].bindVariable(
updateStmt, objects[i], column+1+j);
}
updateStmt.addBatch();
}
int rc[] = updateStmt.executeBatch();
for (int k = 0; k < rc.length; k++) {
nUpdated += rc[k];
}
updateStmt.close();
return nUpdated;
}
/**
* Delete record with specified value of primary key from the table.
*
* @param obj object containing value of primary key.
*/
public synchronized int delete (Connection conn, T obj)
throws SQLException
{
if (primaryKeys == null) {
throw new IllegalStateException(
"No primary key for table " + name + ".");
}
int nDeleted = 0;
StringBuilder sql = new StringBuilder(
"delete from " + name + " where " + primaryKeys[0] + " = ?");
for (int i = 1; i < primaryKeys.length; i++) {
sql.append(" and ").append(primaryKeys[i]).append(" = ?");
}
PreparedStatement deleteStmt = conn.prepareStatement(sql.toString());
for (int i = 0; i < primaryKeys.length; i++) {
fields[primaryKeyIndices[i]].bindVariable(deleteStmt, obj,i+1);
}
nDeleted = deleteStmt.executeUpdate();
deleteStmt.close();
return nDeleted;
}
/**
* Delete records with specified primary keys from the table.
*
* @param objects array of objects containing values of primary key.
*
* @return number of objects actually deleted
*/
public synchronized int delete (Connection conn, T[] objects)
throws SQLException
{
if (primaryKeys == null) {
throw new IllegalStateException(
"No primary key for table " + name + ".");
}
int nDeleted = 0;
StringBuilder sql = new StringBuilder(
"delete from " + name + " where " + primaryKeys[0] + " = ?");
for (int i = 1; i < primaryKeys.length; i++) {
sql.append(" and ").append(primaryKeys[i]).append(" = ?");
}
PreparedStatement deleteStmt = conn.prepareStatement(sql.toString());
for (int i = 0; i < objects.length; i++) {
for (int j = 0; j < primaryKeys.length; j++) {
fields[primaryKeyIndices[j]].bindVariable(
deleteStmt, objects[i], j+1);
}
deleteStmt.addBatch();
}
int rc[] = deleteStmt.executeBatch();
for (int k = 0; k < rc.length; k++) {
nDeleted += rc[k];
}
deleteStmt.close();
return nDeleted;
}
@Override
public String toString ()
{
return "[name=" + name +
", primaryKeys=" + StringUtil.toString(primaryKeys) + "]";
}
/**
* Spearator of name components of compound field. For example, if Java
* class constains component "location" of Point class, which has two
* components "x" and "y", then database table should have columns
* "location_x" and "location_y" (if '_' is used as separator).
*/
public static final String fieldSeparator = "_";
protected final void init (Class<T> clazz, String tableName, String[] keys,
boolean mixedCaseConvert)
{
name = tableName;
this.mixedCaseConvert = mixedCaseConvert;
_rowClass = clazz;
primaryKeys = keys;
listOfFields = "";
qualifiedListOfFields = "";
listOfAssignments = "";
ArrayList<FieldDescriptor> fieldsVector =
new ArrayList<FieldDescriptor>();
nFields = buildFieldsList(fieldsVector, _rowClass, "");
fields = fieldsVector.toArray(new FieldDescriptor[nFields]);
fMask = new FieldMask(fields);
try {
constructor = _rowClass.getDeclaredConstructor(new Class<?>[0]);
setBypass.invoke(constructor, bypassFlag);
} catch(Exception ex) {}
if (keys != null && keys.length > 0) {
primaryKeyIndices = new int[keys.length];
for (int j = keys.length; --j >= 0;) {
int i = nFields;
while (--i >= 0) {
if (fields[i].name.equals(keys[j])) {
if (!fields[i].isAtomic()) {
throw new IllegalArgumentException(
"Non-atomic primary key provided");
}
primaryKeyIndices[j] = i;
break;
}
}
if (i < 0) {
throw new NoSuchFieldError("No such field '" + keys[j]
+ "' in table " + name);
}
}
}
}
protected final String convertName (String name)
{
if (mixedCaseConvert) {
return StringUtil.unStudlyName(name);
} else {
return name;
}
}
protected final int buildFieldsList (ArrayList<FieldDescriptor> buf,
Class<?> _rowClass, String prefix)
{
Field[] f = _rowClass.getDeclaredFields();
Class<?> superclass = _rowClass;
while ((superclass = superclass.getSuperclass()) != null) {
Field[] inheritedFields = superclass.getDeclaredFields();
Field[] allFields = new Field[inheritedFields.length + f.length];
System.arraycopy(inheritedFields, 0, allFields, 0,
inheritedFields.length);
System.arraycopy(f,0, allFields, inheritedFields.length, f.length);
f = allFields;
}
try {
for (int i = f.length; --i>= 0;) {
setBypass.invoke(f[i], bypassFlag);
}
} catch (IllegalAccessException iae) {
System.err.println("Failed to set bypass attribute: " + iae);
} catch (InvocationTargetException ite) {
System.err.println("Failed to set bypass attribute: " + ite);
}
int n = 0;
for (int i = 0; i < f.length; i++) {
if ((f[i].getModifiers()&(Modifier.TRANSIENT|Modifier.STATIC))==0)
{
String name = f[i].getName();
Class<?> fieldClass = f[i].getType();
String fullName = prefix + convertName(name);
FieldDescriptor fd = new FieldDescriptor(f[i], fullName);
int type;
buf.add(fd);
n += 1;
String c = fieldClass.getName();
if (c.equals("byte")) type = FieldDescriptor.t_byte;
else if (c.equals("short")) type = FieldDescriptor.t_short;
else if (c.equals("int")) type = FieldDescriptor.t_int;
else if (c.equals("long")) type = FieldDescriptor.t_long;
else if (c.equals("float")) type = FieldDescriptor.t_float;
else if (c.equals("double")) type = FieldDescriptor.t_double;
else if (c.equals("boolean")) type = FieldDescriptor.t_boolean;
else if (c.equals("java.lang.Byte"))
type = FieldDescriptor.tByte;
else if (c.equals("java.lang.Short"))
type = FieldDescriptor.tShort;
else if (c.equals("java.lang.Integer"))
type = FieldDescriptor.tInteger;
else if (c.equals("java.lang.Long"))
type = FieldDescriptor.tLong;
else if (c.equals("java.lang.Float"))
type = FieldDescriptor.tFloat;
else if (c.equals("java.lang.Double"))
type = FieldDescriptor.tDouble;
else if (c.equals("java.lang.Boolean"))
type = FieldDescriptor.tBoolean;
else if (c.equals("java.math.BigDecimal"))
type = FieldDescriptor.tDecimal;
else if (c.equals("java.lang.String"))
type = FieldDescriptor.tString;
else if (fieldClass.equals(BYTE_PROTO.getClass()))
type = FieldDescriptor.tBytes;
else if (c.equals("java.sql.Date"))
type = FieldDescriptor.tDate;
else if (c.equals("java.sql.Time"))
type = FieldDescriptor.tTime;
else if (c.equals("java.sql.Timestamp"))
type = FieldDescriptor.tTimestamp;
else if (c.equals("java.lang.InputStream"))
type = FieldDescriptor.tStream;
else if (c.equals("java.sql.BlobLocator"))
type = FieldDescriptor.tBlob;
else if (c.equals("java.sql.ClobLocator"))
type = FieldDescriptor.tClob;
else if (serializableClass.isAssignableFrom(fieldClass))
type = FieldDescriptor.tClosure;
else {
int nComponents = buildFieldsList(buf, fieldClass,
fd.name+fieldSeparator);
fd.inType = fd.outType =
FieldDescriptor.tCompound + nComponents;
try {
fd.constructor =
fieldClass.getDeclaredConstructor(new Class<?>[0]);
setBypass.invoke(fd.constructor, bypassFlag);
} catch(Exception ex) {}
n += nComponents;
continue;
}
if (listOfFields.length() != 0) {
listOfFields += ",";
qualifiedListOfFields += ",";
listOfAssignments += ",";
}
listOfFields += fullName;
qualifiedListOfFields += this.name + "." + fullName;
listOfAssignments += fullName + "=?";
fd.inType = fd.outType = type;
nColumns += 1;
}
}
return n;
}
protected final String buildListOfAssignments (FieldMask mask)
{
StringBuilder sql = new StringBuilder();
int fcount = fields.length;
for (int i = 0; i < fcount; i++) {
// skip non-modified fields
if (!mask.isModified(i)) {
continue;
}
// separate fields by a comma
if (sql.length() > 0) {
sql.append(",");
}
// append the necessary SQL to update this column
sql.append(fields[i].name).append("=?");
}
return sql.toString();
}
protected final T load (ResultSet result) throws SQLException
{
T obj;
try {
obj = constructor.newInstance(constructorArgs);
}
catch(IllegalAccessException ex) { throw new IllegalAccessError(); }
catch(InstantiationException ex) { throw new InstantiationError(); }
catch(Exception ex) {
throw new InstantiationError("Exception was thrown by constructor");
}
load(obj, 0, nFields, 0, result);
return obj;
}
protected final int load (
Object obj, int i, int end, int column, ResultSet result)
throws SQLException
{
try {
while (i < end) {
FieldDescriptor fd = fields[i++];
if (!fd.loadVariable(result, obj, ++column)) {
Object component =
fd.constructor.newInstance(constructorArgs);
fd.field.set(obj, component);
int nComponents = fd.inType - FieldDescriptor.tCompound;
column = load(component, i, i + nComponents,
column-1, result);
i += nComponents;
}
}
}
catch(IllegalAccessException ex) { throw new IllegalAccessError(); }
catch(InstantiationException ex) { throw new InstantiationError(); }
catch(InvocationTargetException ex) {
throw new InstantiationError("Exception was thrown by constructor");
}
return column;
}
protected final int bindUpdateVariables(PreparedStatement pstmt,
T obj,
FieldMask mask)
throws SQLException
{
return bindUpdateVariables(pstmt, obj, 0, nFields, 0, mask);
}
protected final void bindQueryVariables(PreparedStatement pstmt,
T obj,
FieldMask mask)
throws SQLException
{
bindQueryVariables(pstmt, obj, 0, nFields, 0, mask);
}
protected final void updateVariables(ResultSet result, T obj)
throws SQLException
{
updateVariables(result, obj, 0, nFields, 0);
result.updateRow();
}
protected final String buildUpdateWhere()
{
StringBuilder sql = new StringBuilder();
sql.append(" where ").append(primaryKeys[0]).append(" = ?");
for (int i = 1; i < primaryKeys.length; i++) {
sql.append(" and ").append(primaryKeys[i]).append(" = ?");
}
return sql.toString();
}
protected final String buildQueryList(T qbe, FieldMask mask, boolean like)
{
StringBuilder buf = new StringBuilder();
buildQueryList(buf, qbe, 0, nFields, mask, like);
if (buf.length() > 0) {
buf.insert(0, " where ");
}
return "select " + listOfFields + " from " + name + buf;
}
protected final int bindUpdateVariables (
PreparedStatement pstmt, Object obj, int i, int end, int column,
FieldMask mask)
throws SQLException
{
try {
while (i < end) {
FieldDescriptor fd = fields[i++];
Object comp = null;
// skip non-modified fields
if (mask != null && !mask.isModified(i-1)) {
continue;
}
if (!fd.isBuiltin() && (comp = fd.field.get(obj)) == null) {
if (fd.isCompound()) {
int nComponents = fd.outType-FieldDescriptor.tCompound;
while (--nComponents >= 0) {
fd = fields[i++];
if (!fd.isCompound()) {
pstmt.setNull(++column,
FieldDescriptor.sqlTypeMapping[fd.outType]);
}
}
} else {
pstmt.setNull(
++column,
FieldDescriptor.sqlTypeMapping[fd.outType]);
}
} else {
if (!fd.bindVariable(pstmt, obj, ++column)) {
int nComponents = fd.outType-FieldDescriptor.tCompound;
column = bindUpdateVariables(
pstmt, comp, i, i+nComponents,column-1, mask);
i += nComponents;
}
}
}
} catch(IllegalAccessException ex) { throw new IllegalAccessError(); }
return column;
}
protected final int bindQueryVariables (
PreparedStatement pstmt, Object obj, int i, int end, int column,
FieldMask mask)
throws SQLException
{
try {
while (i < end) {
Object comp;
FieldDescriptor fd = fields[i++];
if (!fd.field.getDeclaringClass().isInstance(obj)) {
return column;
}
int nComponents = fd.isCompound() ?
fd.outType-FieldDescriptor.tCompound : 0;
try {
// skip closure fields (because querying by them makes
// no sense)
if (fd.outType == FieldDescriptor.tClosure) {
continue;
}
// if a field mask is specified, use that to determine
// whether or not the field should be skipped
if (mask != null) {
if (!mask.isModified(i-1)) {
continue;
}
}
// look up the value of the field
comp = fd.field.get(obj);
// if no field mask was specified, ignore builtin
// fields and those that are null
if (mask == null && (fd.isBuiltin() || comp == null)) {
continue;
}
if (!fd.bindVariable(pstmt, obj, ++column)) {
column = bindQueryVariables(
pstmt, comp, i, i+nComponents, column-1, mask);
}
} finally {
i += nComponents;
}
}
} catch(IllegalAccessException ex) { throw new IllegalAccessError(); }
return column;
}
protected final void buildQueryList (
StringBuilder buf, Object qbe, int i, int end, FieldMask mask,
boolean like)
{
try {
while (i < end) {
Object comp;
FieldDescriptor fd = fields[i++];
int nComponents =
fd.isCompound() ? fd.outType-FieldDescriptor.tCompound : 0;
try {
// skip closure fields (because querying by them makes
// no sense)
if (fd.outType == FieldDescriptor.tClosure) {
continue;
}
// if a field mask is specified, use that to determine
// whether or not the field should be skipped
if (mask != null) {
if (!mask.isModified(i-1)) {
continue;
}
}
// look up the value of the field
comp = fd.field.get(qbe);
// if no field mask was specified, ignore builtin
// fields and those that are null
if (mask == null && (fd.isBuiltin() || comp == null)) {
continue;
}
if (nComponents != 0) {
buildQueryList(buf, comp, i, i+nComponents, mask, like);
} else {
if (buf.length() != 0) {
buf.append(" AND ");
}
buf.append(fd.name);
if (like && (comp instanceof String)) {
buf.append(" like?");
} else {
buf.append("=?");
}
}
} finally {
i += nComponents;
}
}
} catch(IllegalAccessException ex) { throw new IllegalAccessError(); }
}
protected final int updateVariables (
ResultSet result, Object obj, int i, int end, int column)
throws SQLException
{
try {
while (i < end) {
FieldDescriptor fd = fields[i++];
Object comp = null;
if (!fd.isBuiltin() && (comp = fd.field.get(obj)) == null) {
if (fd.isCompound()) {
int nComponents = fd.outType-FieldDescriptor.tCompound;
while (--nComponents >= 0) {
fd = fields[i++];
if (!fd.isCompound()) {
result.updateNull(++column);
}
}
} else {
result.updateNull(++column);
}
} else {
if (!fd.updateVariable(result, obj, ++column)) {
int nComponents = fd.outType-FieldDescriptor.tCompound;
column = updateVariables(result, comp,
i, i+nComponents, column-1);
i += nComponents;
}
}
}
} catch(IllegalAccessException ex) { throw new IllegalAccessError(); }
return column;
}
protected static Method getSetBypass ()
{
try {
Class<?> c = Class.forName("java.lang.reflect.AccessibleObject");
return c.getMethod("setAccessible", new Class<?>[] { Boolean.TYPE });
} catch (Exception ex) {
System.err.println("Unable to reflect AccessibleObject.setAccessible: " + ex);
return null;
}
}
protected String name;
protected String listOfFields;
protected String qualifiedListOfFields;
protected String listOfAssignments;
protected Class<T> _rowClass;
protected boolean mixedCaseConvert = false;
protected FieldDescriptor[] fields;
protected FieldMask fMask;
protected int nFields; // length of "fields" array
protected int nColumns; // number of atomic fields in "fields" array
protected String primaryKeys[];
protected int primaryKeyIndices[];
protected Constructor<T> constructor;
protected static final Method setBypass = getSetBypass();
protected static final Class<Serializable> serializableClass = Serializable.class;
protected static final Object[] bypassFlag = { Boolean.TRUE };
protected static final Object[] constructorArgs = {};
// used to identify byte[] fields
protected static final byte[] BYTE_PROTO = new byte[0];
}
@@ -0,0 +1,58 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
$Id: package.html,v 1.1 2001/08/12 01:34:30 mdb Exp $
samskivert library - useful routines for java programs
Copyright (C) 2001 Michael Bayne
This library 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 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-->
</head>
<body bgcolor="white">
Provides a mapping between JDBC tables and Java objects.
<p> This is a hacked version of JORA 2.06, a library for automatically
mapping RDBMS database rows to Java objects and vice versa. It's
simple, it uses reflection and doesn't require any classfile
post-processing or external configuration information.
<p> "That's all great but why is it hacked?" wonders the astute
reader. Well, I couldn't quite cope with the error handling paradigm
that it used (catch all SQL exceptions internally and pass them to
an application-wide error handler method) and a few seconds thought
on the matter led me to believe that there is not a simple way to
allow a choice of error handling paradigms (the methods have to
throw SQLException or not). So I took the easy way out and forked
the codebase.
<p> The code is pretty stable, so I don't expect to be merging in
updates with any frequency. I promise to be a good monkey and submit
any useful modifications that I make back to the original
maintainer. Fortunately the license was unrestrictive enough to
allow me to do this, otherwise I'd probably be stuck reinventing the
wheel or using something that does less of what I want. Three cheers
for free software.
<p> The JORA package was written by Konstantin Knizhnik and the
original is available from
<a href="http://www.ispras.ru/~knizhnik/">his web site.</a>
</body>
</html>
@@ -0,0 +1,33 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
$Id: package.html,v 1.1 2001/08/12 01:34:30 mdb Exp $
samskivert library - useful routines for java programs
Copyright (C) 2001 Michael Bayne
This library 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 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-->
</head>
<body bgcolor="white">
Provides support services to applications that access relational
databases via JDBC.
</body>
</html>
@@ -0,0 +1,104 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.net;
import java.net.URL;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.HashMap;
import static com.samskivert.Log.log;
/**
* Allows other entities in an application to register URLStreamHandler
* classes for protocols of their own making.
*/
public class AttachableURLFactory implements URLStreamHandlerFactory
{
/**
* Register a URL handler.
*
* @param protocol the protocol to register.
* @param handlerClass a Class of type java.net.URLStreamHandler
*/
public static void attachHandler (
String protocol, Class<? extends URLStreamHandler> handlerClass)
{
// set up the factory.
if (_handlers == null) {
_handlers = new HashMap<String,Class<? extends URLStreamHandler>>();
// There are two ways to do this.
// Method 1, which is the only one that seems to work under
// Java Web Start, is to register a factory. This can throw an
// Error if another factory is already registered. We let that
// error bubble on back.
URL.setURLStreamHandlerFactory(new AttachableURLFactory());
// Method 2 seems like a better idea but doesn't work under
// Java Web Start. We add on a property that registers this
// very class as the handler for the resource property. It
// would be instantiated with Class.forName().
// (And I did check, it's not dasho that is preventing this
// from working under JWS, it's something else.)
/*
// dug up from java.net.URL
String HANDLER_PROP = "java.protocol.handler.pkgs";
String prop = System.getProperty(HANDLER_PROP, "");
if (!"".equals(prop)) {
prop += "|";
}
prop += "com.threerings";
System.setProperty(HANDLER_PROP, prop);
*/
}
_handlers.put(protocol.toLowerCase(), handlerClass);
}
/**
* Do not let others instantiate us.
*/
private AttachableURLFactory ()
{
}
// documentation inherited from interface URLStreamHandlerFactory
public URLStreamHandler createURLStreamHandler (String protocol)
{
Class<? extends URLStreamHandler> handler = _handlers.get(protocol.toLowerCase());
if (handler != null) {
try {
return handler.newInstance();
} catch (Exception e) {
log.warning("Unable to instantiate URLStreamHandler", "protocol", protocol,
"cause", e);
}
}
return null;
}
/** A mapping of protocol name to handler classes. */
protected static HashMap<String,Class<? extends URLStreamHandler>> _handlers;
}
@@ -0,0 +1,96 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.net;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import com.samskivert.util.ServiceWaiter;
/**
* Contains utility methods for doing a form post.
*/
public class HttpPostUtil
{
/**
* Return the results of a form post. Note that the http request takes
* place on another thread, but this thread blocks until the results
* are returned or it times out.
*
* @param url from which to make the request.
* @param submission the entire submission eg "foo=bar&baz=boo&futz=foo".
* @param timeout time to wait for the response, in seconds, or -1
* for forever.
*/
public static String httpPost (final URL url, final String submission,
int timeout)
throws IOException, ServiceWaiter.TimeoutException
{
final ServiceWaiter<String> waiter = new ServiceWaiter<String>(
(timeout < 0) ? ServiceWaiter.NO_TIMEOUT : timeout);
Thread tt = new Thread() {
@Override public void run () {
try {
HttpURLConnection conn =
(HttpURLConnection) url.openConnection();
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestProperty(
"Content-Type", "application/x-www-form-urlencoded");
DataOutputStream out = new DataOutputStream(
conn.getOutputStream());
out.writeBytes(submission);
out.flush();
out.close();
BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
StringBuilder buf = new StringBuilder();
for (String s; null != (s = reader.readLine()); ) {
buf.append(s);
}
reader.close();
waiter.postSuccess(buf.toString()); // yay
} catch (IOException e) {
waiter.postFailure(e); // boo
}
}
};
tt.start();
if (waiter.waitForResponse()) {
return waiter.getArgument();
} else {
throw (IOException) waiter.getError();
}
}
}
@@ -0,0 +1,171 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.net;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.samskivert.util.RunAnywhere;
/**
* Attempts to find all the MAC addresses on the machine.
* This is accomplished by calling external programs specific
* to the platform we are on, and parsing the results.
*/
public class MACUtil
{
/**
public static void main (String[] args)
{
String testOutput = "YOUR TEST STRING GOES HERE";
String[] macs = parseMACs(testOutput);
for (int ii = 0; ii < macs.length; ii++) {
System.err.println("mac: " + macs[ii]);
}
}
*/
/**
* Get all the MAC addresses of the hardware we are running on that we can find.
*/
public static String[] getMACAddresses ()
{
String [] cmds;
if (RunAnywhere.isWindows()) {
cmds = WINDOWS_CMDS;
} else {
cmds = UNIX_CMDS;
}
return parseMACs(tryCommands(cmds));
}
/**
* Look through the text for all the MAC addresses we can find.
*/
protected static String[] parseMACs (String text)
{
Matcher m = MACRegex.matcher(text);
ArrayList<String> list = new ArrayList<String>();
while (m.find()) {
String mac = m.group(1).toUpperCase();
mac = mac.replace(':', '-');
// "Didn't you get that memo?" Apparently some people are not
// up on MAC addresses actually being unique, so we will ignore those.
//
// 44-45-53-XX-XX-XX - PPP Adaptor
// 00-53-45-XX-XX-XX - PPP Adaptor
// 00-E0-06-09-55-66 - Some bogus run of ASUS motherboards
// 00-04-4B-80-80-03 - Some nvidia built-in lan issues
// 00-03-8A-XX-XX-XX - MiniWAN or AOL software
// 02-03-8A-00-00-11 - Westell Dual (USB/Ethernet) modem
// FF-FF-FF-FF-FF-FF - Tunnel adapter Teredo
// 02-00-4C-4F-4F-50 - MSFT thinger, loopback of some sort
if (mac.startsWith("44-45-53")) {
continue;
} else if (mac.startsWith("00-53-45-00")) {
continue;
} else if (mac.startsWith("00-E0-06-09-55-66")) {
continue;
} else if (mac.startsWith("00-04-4B-80-80-03")) {
continue;
} else if (mac.startsWith("00-03-8A")) {
continue;
} else if (mac.startsWith("02-03-8A-00-00-11")) {
continue;
} else if (mac.startsWith("FF-FF-FF-FF-FF-FF")) {
continue;
} else if (mac.startsWith("02-00-4C-4F-4F-50")) {
continue;
}
list.add(mac);
}
return list.toArray(new String[0]);
}
/**
* Takes a lists of commands and tries to run each one till one works.
* The idea being to be able to 'gracefully' cope with not knowing
* where a command is installed on different installations.
*/
protected static String tryCommands (String[] cmds)
{
if (cmds == null) {
return null;
}
String output;
for (int ii = 0; ii < cmds.length; ii++) {
output = runCommand(cmds[ii]);
if (output != null) {
return output;
}
}
return null;
}
/**
* Run the specified command and return the output as a string.
*/
protected static String runCommand (String cmd)
{
try {
Process p = Runtime.getRuntime().exec(cmd);
BufferedReader cin = new BufferedReader(new InputStreamReader(p.getInputStream()));
StringBuilder buffer= new StringBuilder();
String line = "";
while (line != null)
{
buffer.append(line);
line = cin.readLine();
}
cin.close();
return buffer.toString();
} catch (IOException e) {
// don't want to log anything for the client to know what we are doing.
// This will almost always be thrown/caught when the cmd isn't there.
return null;
}
}
/** Look for 2 hex values in a row followed by a ':' or '-' repeated 5 times, followed by 2 hex
* values. */
protected static final Pattern MACRegex =
Pattern.compile("((?:\\p{XDigit}{2}+[:\\-]){5}+\\p{XDigit}{2}+)", Pattern.CASE_INSENSITIVE);
// TODO maybe we should obfuscate these with rot13 or something so
// that people can't run 'strings' on us and instantly see what we try
protected static final String[] WINDOWS_CMDS = {"ipconfig /all"};
protected static final String[] UNIX_CMDS = {"/sbin/ifconfig", "/etc/ifconfig"};
}
@@ -0,0 +1,175 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.net;
import java.io.IOException;
import java.util.Properties;
import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.regex.Pattern;
import com.samskivert.util.StringUtil;
/**
* The mail util class encapsulates some utility functions related to
* sending emails.
*/
public class MailUtil
{
/**
* Checks the supplied address against a regular expression that (for
* the most part) matches only valid email addresses. It's not
* perfect, but the omissions and inclusions are so obscure that we
* can't be bothered to worry about them.
*/
public static boolean isValidAddress (String address)
{
return _emailre.matcher(address).matches();
}
/**
* @return a normalized form of the specified email address
* (everything after the @ sign is lowercased).
*/
public static String normalizeAddress (String address)
{
// this algorithm is tolerant of bogus input: if address has no
// @ symbol it will cope.
int afterAt = address.indexOf('@') + 1;
return address.substring(0, afterAt) +
address.substring(afterAt).toLowerCase();
}
/**
* Delivers the supplied mail message using the machine's local mail
* SMTP server which must be listening on port 25.
*
* @exception IOException thrown if an error occurs delivering the
* email. See {@link Transport#send} for exceptions that can be thrown
* due to response from the SMTP server.
*/
public static void deliverMail (String recipient, String sender,
String subject, String body)
throws IOException
{
deliverMail(new String[] { recipient }, sender, subject, body);
}
/**
* Delivers the supplied mail message using the machine's local mail
* SMTP server which must be listening on port 25. If the
* <code>mail.smtp.host</code> system property is set, that will be
* used instead of localhost.
*
* @exception IOException thrown if an error occurs delivering the
* email. See {@link Transport#send} for exceptions that can be thrown
* due to response from the SMTP server.
*/
public static void deliverMail (String[] recipients, String sender,
String subject, String body)
throws IOException
{
deliverMail(recipients, sender, subject, body, null, null);
}
/**
* Delivers the supplied mail, with the specified additional headers.
*/
public static void deliverMail (String[] recipients, String sender,
String subject, String body,
String[] headers, String[] values)
throws IOException
{
if (recipients == null || recipients.length < 1) {
throw new IOException("Must specify one or more recipients.");
}
try {
MimeMessage message = createEmptyMessage();
int hcount = (headers == null) ? 0 : headers.length;
for (int ii = 0; ii < hcount; ii++) {
message.addHeader(headers[ii], values[ii]);
}
message.setText(body);
deliverMail(recipients, sender, subject, message);
} catch (Exception e) {
String errmsg = "Failure sending mail [from=" + sender +
", to=" + StringUtil.toString(recipients) +
", subject=" + subject + "]";
IOException ioe = new IOException(errmsg);
ioe.initCause(e);
throw ioe;
}
}
/**
* Delivers an already-formed message to the specified recipients. This
* message can be any mime type including multipart.
*/
public static void deliverMail (String[] recipients, String sender,
String subject, MimeMessage message)
throws IOException
{
try {
message.setFrom(new InternetAddress(sender));
for (String recipient : recipients) {
message.addRecipient(Message.RecipientType.TO, new InternetAddress(recipient));
}
if (subject != null) {
message.setSubject(subject);
}
Transport.send(message);
} catch (Exception e) {
String errmsg = "Failure sending mail [from=" + sender +
", to=" + StringUtil.toString(recipients) +
", subject=" + subject + "]";
IOException ioe = new IOException(errmsg);
ioe.initCause(e);
throw ioe;
}
}
/**
* Returns an initialized, but empty message.
*/
public static final MimeMessage createEmptyMessage ()
{
Properties props = System.getProperties();
if (props.getProperty("mail.smtp.host") == null) {
props.put("mail.smtp.host", "localhost");
}
return new MimeMessage(Session.getDefaultInstance(props, null));
}
/** Originally formulated by lambert@nas.nasa.gov. */
protected static final String EMAIL_REGEX = "^([-A-Za-z0-9_.!%+]+@" +
"[-a-zA-Z0-9]+(\\.[-a-zA-Z0-9]+)*\\.[-a-zA-Z0-9]+)$";
/** A compiled version of our email regular expression. */
protected static final Pattern _emailre = Pattern.compile(EMAIL_REGEX);
}
@@ -0,0 +1,125 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.net;
import java.io.File;
import java.io.IOException;
/**
* Path related utility functions.
*/
public class PathUtil
{
/**
* Replaces the final component in the supplied path with the
* specified new component. For example, if <code>/foo/bar/baz</code>
* was provided as the source path, <code>baz</code> would be replaced
* with the supplied new path component. If no slashes occur in the
* path, the entire path will be replaced. Note that this function is
* intended for use on URLs rather than filesystem paths and thus
* always uses forward slash rather than the platform defined path
* separator.
*/
public static String replaceFinalComponent (String source,
String newComponent)
{
int sidx = source.lastIndexOf("/");
if (sidx != -1) {
return source.substring(0, sidx+1) + newComponent;
} else {
return newComponent;
}
}
/**
* Appends the supplied affix to the specified source path, ensuring
* that exactly one path separator (<code>/</code> as we are dealing
* with URLs here not platform specific file-system paths) is used
* between the two. <em>Note:</em> this means that the affix will be
* made into a relative path regardless of whether or not it starts
* with a <code>/</code>.
*/
public static String appendPath (String source, String affix)
{
if (source.endsWith("/")) {
if (affix.startsWith("/")) {
return source + affix.substring(1);
} else {
return source + affix;
}
} else if (affix.startsWith("/")) {
return source + affix;
} else {
return source + "/" + affix;
}
}
/**
* Gets the individual path elements building up the canonical path to the given file.
*/
public static String[] getCanonicalPathElements (File file)
throws IOException
{
file = file.getCanonicalFile();
// If we were a file, get its parent
if (!file.isDirectory()) {
file = file.getParentFile();
}
return file.getPath().split(File.separator);
}
/**
* Computes a relative path between to Files
*
* @param file the file we're referencing
* @param relativeTo the path from which we want to refer to the file
*/
public static String computeRelativePath (File file, File relativeTo)
throws IOException
{
String[] realDirs = getCanonicalPathElements(file);
String[] relativeToDirs = getCanonicalPathElements(relativeTo);
// Eliminate the common root
int common = 0;
for (; common < realDirs.length && common < relativeToDirs.length; common++) {
if (!realDirs[common].equals(relativeToDirs[common])) {
break;
}
}
String relativePath = "";
// For each remaining level in the file path, add a ..
for (int ii = 0; ii < (realDirs.length - common); ii++) {
relativePath += ".." + File.separator;
}
// For each level in the resource path, add the path
for (; common < relativeToDirs.length; common++) {
relativePath += relativeToDirs[common] + File.separator;
}
return relativePath;
}
}
@@ -0,0 +1,470 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.net.cddb;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
/**
* The CDDB class provides access to the information provided by servers compliant with the
* <a href="http://www.freedb.org/software/old/CDDBPROTO">CDDB protocol</a>.
*/
public class CDDB
{
/**
* The port on which CDDB servers are generally run.
*/
public static final int STANDARD_PORT = 888;
/**
* The client name reported to the CDDB server.
*/
public static final String CLIENT_NAME = "TSP/CDDB_client";
/**
* The client version reported to the CDDB server.
*/
public static final String CLIENT_VERSION = "1.0";
/**
* This class encapsulates the information needed to look up a full CDDB record for a
* particular disc. An array of them are returned in response to a CDDB query.
*/
public static class Entry
{
/** The category to which this entry belongs. */
public String category;
/** The unique identifier for this entry. */
public String cdid;
/** The human readable title of this entry. */
public String title;
/**
* Parses values for this entry from the supplied source string. The source string should
* contain an entry description as formatted by the CDDB server.
*
* @exception CDDBException Thrown if the entry is not properly formatted.
*/
public void parse (String source)
throws CDDBException
{
int sidx1 = source.indexOf(" ");
int sidx2 = source.indexOf(" ", sidx1+1);
if (sidx1 == -1 || sidx2 == -1) {
throw new CDDBException(499, "Malformed entry '" + source + "'");
}
category = source.substring(0, sidx1);
cdid = source.substring(sidx1+1, sidx2);
title = source.substring(sidx2+1);
}
}
/**
* Connects this CDDB instance to the CDDB server running on the supplied host using the
* standard port.
*
* <p> <b>Note well:</b> you must close a CDDB connection once you are done with it, otherwise
* the socket connection will remain open and pointlessly consume machine resources.
*
* @param hostname The host to which to connect.
*
* @exception IOException Thrown if a network error occurs attempting to connect to the host.
* @exception CDDBException Thrown if an error occurs after identifying ourselves to the host.
*
* @return The message supplied with the succesful connection response.
*
* @see #STANDARD_PORT
* @see #close
*/
public String connect (String hostname)
throws IOException, CDDBException
{
return connect(hostname, STANDARD_PORT);
}
/**
* Connects this CDDB instance to the CDDB server running on the supplied host using the
* specified port.
*
* <p> <b>Note well:</b> you must close a CDDB connection once you are done with it, otherwise
* the socket connection will remain open and pointlessly consume machine resources.
*
* @param hostname The host to which to connect.
* @param port The port number on which to connect to the host.
*
* @exception IOException Thrown if a network error occurs attempting to connect to the host.
* @exception CDDBException Thrown if an error occurs after identifying ourselves to the host.
*
* @return The message supplied with the succesful connection response.
*
* @see #close
*/
public String connect (String hostname, int port)
throws IOException, CDDBException
{
// obtain the necessary information we'll need to identify
// ourselves to the CDDB server
String localhost = InetAddress.getLocalHost().getHostName();
String username = System.getProperty("user.name");
if (username == null) {
username = "anonymous";
}
// establish our socket connection and IO streams
InetAddress addr = InetAddress.getByName(hostname);
_sock = new Socket(addr, port);
_in = new BufferedReader(new InputStreamReader(_sock.getInputStream()));
_out = new PrintStream(new BufferedOutputStream(_sock.getOutputStream()));
// first read (and discard) the banner string
_in.readLine();
// send a hello request
StringBuilder req = new StringBuilder("cddb hello ");
req.append(username).append(" ");
req.append(localhost).append(" ");
req.append(CLIENT_NAME).append(" ");
req.append(CLIENT_VERSION);
Response rsp = request(req.toString());
// confirm that the response was a successful one
if (CDDBProtocol.codeFamily(rsp.code) != CDDBProtocol.OK &&
rsp.code != 402 /* already shook hands */) {
throw new CDDBException(rsp.code, rsp.message);
}
return rsp.message;
}
/**
* Specifies the number of milliseconds that the client should wait for a response from the
* server before aborting. This must be called <em>after</em> a successful call to connect,
* otherwise the timeout will not be set.
*/
public void setTimeout (int timeout)
throws SocketException
{
if (_sock != null) {
_sock.setSoTimeout(timeout);
}
}
/**
* @return true if this CDDB instance is connected to a CDDB server.
*/
public boolean connected ()
{
return _sock != null;
}
/**
* Closes the connection to the CDDB server previously opened with a call to {@link #connect}.
* If the connection was not previously established, this member function does nothing.
*/
public void close ()
throws IOException
{
if (_sock != null) {
_sock.close();
// clear out our references
_sock = null;
_in = null;
_out = null;
}
}
/**
* Issues a query to the CDDB server using the supplied CD identifying information.
*
* @param discid The disc identifier (information on how to compute the disc ID is available <a
* href="http://www.freedb.org/sections.php?op=viewarticle&artid=6"> here</a>.
* @param frameOffsets The frame offset of each track. The length of this array is assumed to
* be the number of tracks on the CD and is used in the query.
* @param length The length (in seconds) of the CD.
*
* @return If no entry matches the query, null is returned. Otherwise one or more entries is
* returned that matched the query parameters.
*/
public Entry[] query (String discid, int[] frameOffsets, int length)
throws IOException, CDDBException
{
// sanity check
if (_sock == null) {
throw new CDDBException(500, "Not connected");
}
// construct the query parameter
StringBuilder req = new StringBuilder("cddb query ");
req.append(discid).append(" ");
req.append(frameOffsets.length).append(" ");
for (int frameOffset : frameOffsets) {
req.append(frameOffset).append(" ");
}
req.append(length);
// make the request
Response rsp = request(req.toString());
Entry[] entries = null;
// if this is an exact match, parse the entry and return it
if (rsp.code == 200 /* exact match */) {
entries = new Entry[1];
entries[0] = new Entry();
entries[0].parse(rsp.message);
} else if (rsp.code == 211 /* inexact matches */) {
// read the matches from the server
ArrayList<Entry> list = new ArrayList<Entry>();
String input = _in.readLine();
while (input != null && !input.equals(CDDBProtocol.TERMINATOR)) {
System.out.println("...: " + input);
Entry e = new Entry();
e.parse(input);
list.add(e);
input = _in.readLine();
}
entries = new Entry[list.size()];
list.toArray(entries);
} else if (CDDBProtocol.codeFamily(rsp.code) != CDDBProtocol.OK) {
throw new CDDBException(rsp.code, rsp.message);
}
return entries;
}
/**
* A detail object contains all of the detailed information about a particular CD as retrieved
* from the CDDB server.
*/
public static class Detail
{
/** The unique identifier for this CD. */
public String discid;
/** The category to which this CD belongs. */
public String category;
/** The title of this CD. */
public String title;
/** The track names. */
public String[] trackNames;
/** The extended data for the CD. */
public String extendedData;
/** The extended data for each track. */
public String[] extendedTrackData;
}
/**
* Requests the detail information for a particular disc in a particular category from the CDDB
* server.
*
* @return A detail instance filled with the information for the requested CD.
*/
public Detail read (String category, String discid)
throws IOException, CDDBException
{
// sanity check
if (_sock == null) {
throw new CDDBException(500, "Not connected");
}
// construct the query
StringBuilder req = new StringBuilder("cddb read ");
req.append(category).append(" ");
req.append(discid);
// make the request
Response rsp = request(req.toString());
// anything other than an OK response earns an exception
if (rsp.code != 210 /* OK, entry follows */) {
throw new CDDBException(rsp.code, rsp.message);
}
Detail detail = new Detail();
// parse the category and discid from the response string
int sidx = rsp.message.indexOf(" ");
if (sidx == -1) {
throw new CDDBException(500, "Malformed read response: " +
rsp.message);
}
detail.category = rsp.message.substring(0, sidx);
detail.discid = rsp.message.substring(sidx+1);
ArrayList<String> tnames = new ArrayList<String>();
ArrayList<String> texts = new ArrayList<String>();
// now parse the contents
String input = _in.readLine();
for (int lno = 0; !input.equals(CDDBProtocol.TERMINATOR); lno++) {
if (input.startsWith("#")) {
// skip comments
} else if (input.startsWith("DTITLE")) {
if (detail.title == null) {
detail.title = contents(input, lno);
} else {
detail.title += contents(input, lno);
}
} else if (input.startsWith("EXTD")) {
if (detail.extendedData == null) {
detail.extendedData = contents(input, lno);
} else {
detail.extendedData += contents(input, lno);
}
} else if (input.startsWith("TTITLE")) {
append(tnames, index(input, "TTITLE", lno), contents(input, lno));
} else if (input.startsWith("EXTT")) {
append(texts, index(input, "EXTT", lno), contents(input, lno));
}
// read in the next line of input
input = _in.readLine();
}
// convert the lists into arrays
detail.trackNames = new String[tnames.size()];
tnames.toArray(detail.trackNames);
detail.extendedTrackData = new String[texts.size()];
texts.toArray(detail.extendedTrackData);
return detail;
}
/**
* Extracts the track index of the supplied line (the number immediately following the supplied
* prefix and preceding the equals sign) or throws a CDDBException if the line contains no
* equals sign or track index.
*/
protected final int index (String source, String prefix, int lineno)
throws CDDBException
{
int eidx = source.indexOf("=", prefix.length());
if (eidx == -1) {
throw new CDDBException(500, "Malformed line '" + source + "' number " + lineno);
}
try {
return Integer.parseInt(source.substring(prefix.length(), eidx));
} catch (NumberFormatException nfe) {
throw new CDDBException(500, "Malformed line '" + source + "' number " + lineno);
}
}
/**
* Extracts the contents of the supplied line (everything after the equals sign) or throws a
* CDDBException if the line contains no equals sign.
*/
protected final String contents (String source, int lineno)
throws CDDBException
{
int eidx = source.indexOf("=");
if (eidx == -1) {
throw new CDDBException(500, "Malformed line '" + source + "' number " + lineno);
}
return source.substring(eidx+1);
}
/**
* Appends the supplied string to the contents of the list at the supplied index. If the list
* has no contents at the supplied index, the supplied value becomes the contents at that
* index.
*/
protected final void append (
ArrayList<String> list, int index, String value)
{
// expand the list as necessary
while (list.size() <= index) {
list.add("");
}
list.set(index, list.get(index) + value);
}
/**
* Issues a request to the CDDB server and parses the response.
*/
protected Response request (String req)
throws IOException
{
System.err.println("REQ:" + req);
// send the request to the server
_out.println(req);
_out.flush();
// now read the response
String rspstr = _in.readLine();
// if they closed the connection on us, we should deal
if (rspstr == null) {
throw new EOFException();
}
System.err.println("RSP:" + rspstr);
// parse the response
Response rsp = new Response();
String codestr = rspstr;
// assume the response is just a code unless we see a space
int sidx = rspstr.indexOf(" ");
if (sidx != -1) {
codestr = rspstr.substring(0, sidx);
rsp.message = rspstr.substring(sidx+1);
}
try {
rsp.code = Integer.parseInt(codestr);
} catch (NumberFormatException nfe) {
rsp.code = 599;
rsp.message = "Unparseable response";
}
return rsp;
}
/**
* A simple class to encapsulate the response from the CDDB server.
*/
protected static class Response
{
public int code;
public String message;
}
protected Socket _sock;
protected BufferedReader _in;
protected PrintStream _out;
}
@@ -0,0 +1,45 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.net.cddb;
/**
* This exception class encapsulates errors that may occur while
* communicating to a CDDB server. It is not used to communicate IO errors
* (an IOException is used for that), but it is used to communicate
* failures communicated within the scope of the CDDB protocol.
*
* @see CDDB
*/
public class CDDBException extends Exception
{
public CDDBException (int code, String message)
{
super(message);
_code = code;
}
public int getCode ()
{
return _code;
}
protected int _code;
}
@@ -0,0 +1,68 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.net.cddb;
/**
* The CDDB protocol class provides constants related to the CDDB protocol
* as well as utility routines useful in dealing with the protocol.
*/
public class CDDBProtocol
{
/**
* The terminating marker which occurs alone on it's own line to
* indicate the end of an extended command.
*/
public static final String TERMINATOR = ".";
/* Class constant offsets */
public static final int INFORMATIONAL = 100;
public static final int OK = 200;
public static final int OK_WITH_CONTINUATION = 300;
public static final int UNABLE_TO_PERFORM = 400;
public static final int SERVER_ERROR = 500;
/**
* @return The family to which the supplied specific response code
* belongs.
*/
public static int codeFamily (int code)
{
return (code / 100) * 100;
}
/**
* @return The genus to which the supplied specific response code
* belongs.
*/
public static int codeGenus (int code)
{
return ((code % 100) / 10) * 10;
}
/**
* @return The species to which the supplied specific response code
* belongs.
*/
public static int codeSpecies (int code)
{
return code % 10;
}
}
@@ -0,0 +1,94 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.net.cddb;
/**
* Tests the CDDB code by connecting to a CDDB server and making some
* requests.
*/
public class CDDBTest
{
public static void test (String hostname, String cdid)
throws Exception
{
CDDB cddb = new CDDB();
try {
/* String rsp = */ cddb.connect(hostname);
// set the timeout to 30 seconds
cddb.setTimeout(30*1000);
// try a test query
int[] offsets = { 150, 18130, 48615 };
int length = 893;
CDDB.Entry[] entries = cddb.query(cdid, offsets, length);
if (entries == null || entries.length == 0) {
System.out.println("No match for " + cdid + ".");
} else {
for (int i = 0; i < entries.length; i++) {
System.out.println("Match " + entries[i].category + "/" +
entries[i].cdid + "/" +
entries[i].title);
}
CDDB.Detail detail = cddb.read(entries[0].category,
entries[0].cdid);
System.out.println("Title: " + detail.title);
for (int i = 0; i < detail.trackNames.length; i++) {
System.out.println(pad(i) + ": " + detail.trackNames[i]);
}
System.out.println("Extended data: " + detail.extendedData);
for (int i = 0; i < detail.extendedTrackData.length; i++) {
System.out.println(pad(i) + ": " + detail.extendedTrackData[i]);
}
}
} finally {
cddb.close();
}
}
protected static String pad (int value)
{
return ((value > 9) ? "" : " ") + value;
}
public static void main (String[] args)
{
if (args.length < 1) {
System.err.println("Usage: CDDBTest cddbhost");
System.exit(-1);
}
try {
test(args[0], "1b037b03");
} catch (CDDBException ce) {
System.err.println("Protocol exception: " + ce.getCode() + ": " + ce.getMessage());
} catch (Throwable t) {
t.printStackTrace(System.err);
}
}
}
@@ -0,0 +1,57 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
$Id: package.html,v 1.1 2001/08/12 01:34:31 mdb Exp $
samskivert library - useful routines for java programs
Copyright (C) 2001 Michael Bayne
This library 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 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-->
</head>
<body bgcolor="white">
Routines for accessing a CDDB server.
<p> The {@link com.samskivert.net.cddb.CDDB} class provides the
primary interface to the CDDB services. This service does not
provide a means to obtain the CD identification information that
you'll need to perform a CDDB lookup. In the application that
motivated the creation of these services, a <code>cdparanoia</code>
process was spawned to read the disc info. Other mechanisms surely
exist.
<p> Use of the CDDB class is fairly straightforward:
<pre>
CDDB cddb = new CDDB();
cddb.connect("www.freedb.org");
CDDB.Entry[] entries = cddb.query(discid, frameOffsets, length);
for (int i = 0; i < entries.length; i++) {
CDDB.Detail detail = cddb.read(entries[i].category,
entries[i].discid);
// do your thang now...
}
</pre>
<p> These services presently do not provide a mechanism for submitting
a CDDB entry (which involves sending an email). It surely will in
the future.
</body>
</html>
@@ -0,0 +1,32 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
$Id: package.html,v 1.1 2001/08/12 01:34:31 mdb Exp $
samskivert library - useful routines for java programs
Copyright (C) 2001 Michael Bayne
This library 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 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-->
</head>
<body bgcolor="white">
Provides network related services and patterns.
</body>
</html>
@@ -0,0 +1,74 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
$Id: overview.html,v 1.1 2001/08/12 01:45:46 mdb Exp $
samskivert library - useful routines for java programs
Copyright (C) 2001 Michael Bayne
This library 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 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-->
</head>
<body bgcolor="white">
The samskivert library (SL) aims to provide useful reusable Java
routines that do things for which I've been unable to find useful
reusable implementations on the net.
<p> Given the emphasis on reusability, SL attempts to closely adhere
to the following principles:
<ul>
<li><p> Each individual module should depend as little as possible
on other SL modules. Obvious exceptions include modules that are
a logical extension of other modules and modules that clearly
require a service that is implemented by another SL module and
would have to implement that service themselves in the absence
of dependence on the other module.
<li><p> Modules should be both simple to use and as general purpose
as possible. To meet these two competing requirements, a balance
must be struck at that sweet spot where reusability is
maximized.
<li><p> Code included in SL will freely depend on JDK packages
available in the Java 2 platform and beyond. SL is initially a
repository of software useful for server-side or stand alone
applications and therefore need not make compromises to function
in the jungle of JVMs in commonly available web browsers.
<li><p> We are not here to reinvent the wheel, nor to provide a
uniform interface to every software service under the sun. If
something is available in a freely redistributable and reusable
form from someone else, it won't be found in SL. If SL depends
on such software from another source, it will provide clear
documentation on how to get that software and make use of it
within the scope of SL's particular needs. Again a balance of
reusability will be struck here and software that is
sufficiently difficult to make usable in an arbitrary
environment will not be used by SL and may be "reinvented".
</ul>
<p> I and others who have contributed to the library hope that you
find it useful, easy to use and time saving. Comments and
contributions are always welcome.
<p> -- <a href="mailto:mdb@samskivert.com">mdb@samskivert.com</a>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
$Id: package.html,v 1.1 2001/08/12 01:34:30 mdb Exp $
samskivert library - useful routines for java programs
Copyright (C) 2001 Michael Bayne
This library 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 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-->
</head>
<body bgcolor="white">
Nothing lives here except the {@link com.samskivert.util.Log} wrapper
that is used by the samskivert services to generate log messages.
</body>
</html>
@@ -0,0 +1,66 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet;
import javax.servlet.http.HttpServletResponse;
/**
* An http error exception is thrown by servlet services when they wish to fail
* the request in some particular way (ie. {@link
* HttpServletResponse#SC_FORBIDDEN}, etc.). It is expected that error handling
* can be implemented in a single place such that servlets can simply allow
* this exception to propagate up to the proper handler which will then issue
* the appropriate failure header.
*/
public class HttpErrorException extends Exception
{
public HttpErrorException (int errorCode)
{
super(String.valueOf(errorCode));
_errorCode = errorCode;
}
public HttpErrorException (int errorCode, String errorMessage)
{
super(errorMessage);
_errorCode = errorCode;
}
/**
* Returns the HTTP error code supplied with this exception.
*/
public int getErrorCode ()
{
return _errorCode;
}
/**
* Returns the textual error message supplied with this exception or null
* if only an error code was supplied.
*/
public String getErrorMessage ()
{
String msg = getMessage();
return String.valueOf(_errorCode).equals(msg) ? null : msg;
}
protected int _errorCode;
}
@@ -0,0 +1,69 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet;
import java.util.ArrayList;
import java.util.Iterator;
import javax.servlet.http.HttpServletRequest;
/**
* Used by default and for systems that have no need to discriminate between different sites in
* their web applications.
*/
public class IndiscriminateSiteIdentifier implements SiteIdentifier
{
/**
* Always returns {@link #DEFAULT_SITE_ID} regardless of the information in the request.
*/
public int identifySite (HttpServletRequest req)
{
return DEFAULT_SITE_ID;
}
/**
* Always returns {@link #DEFAULT_SITE_STRING} regardless of the value of the supplied
* identifer.
*/
public String getSiteString (int siteId)
{
return DEFAULT_SITE_STRING;
}
/**
* Always returns {@link #DEFAULT_SITE_ID} regardless of the value of the supplied string.
*/
public int getSiteId (String siteString)
{
return DEFAULT_SITE_ID;
}
// documented inherited from interface
public Iterator<Site> enumerateSites ()
{
return _sites.iterator();
}
protected static ArrayList<Site> _sites = new ArrayList<Site>();
static {
_sites.add(new Site(DEFAULT_SITE_ID, DEFAULT_SITE_STRING));
}
}
@@ -0,0 +1,333 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import javax.servlet.http.HttpServletRequest;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.SimpleRepository;
import com.samskivert.util.ArrayUtil;
import com.samskivert.util.HashIntMap;
import static com.samskivert.Log.log;
/**
* Accomplishes the process of site identification based on a mapping from domains (e.g.
* samskivert.com) to site identifiers that is maintained in a database table, accessible via JDBC
* (hence the name).
*
* <p> There are two tables, one that maps domains to site identifiers and another that maps site
* identifiers to site strings. These are both loaded at construct time and refreshed periodically
* in the course of normal operation.
*
* <p> Note that any of the calls to identify, lookup or enumerate site information can result in
* the sites table being refreshed from the database which will take relatively much longer than
* the simple hashtable lookup that the operations normally require. However, this happens only
* once every 15 minutes and the circumstances in which the site identifier are normally used can
* generally accomodate the extra 100 milliseconds or so that it is likely to take to reload the
* (tiny) sites and domains tables from the database.
*/
public class JDBCTableSiteIdentifier implements SiteIdentifier
{
/** The database identifier used to obtain a connection from our connection provider. The value
* is <code>sitedb</code> which you'll probably need to know to provide the proper
* configuration to your connection provider. */
public static final String SITE_IDENTIFIER_IDENT = "sitedb";
/**
* Constructs a JDBC table site identifier with the supplied connection provider from which to
* obtain its database connection.
*
* @see #SITE_IDENTIFIER_IDENT
*/
public JDBCTableSiteIdentifier (ConnectionProvider conprov)
throws PersistenceException
{
this(conprov, DEFAULT_SITE_ID);
}
/**
* Creates an identifier that will load data from the supplied connection provider and which
* will use the supplied default site id instead of {@link #DEFAULT_SITE_ID}.
*/
public JDBCTableSiteIdentifier (ConnectionProvider conprov, int defaultSiteId)
throws PersistenceException
{
_repo = new SiteIdentifierRepository(conprov);
_repo.refreshSiteData();
_defaultSiteId = defaultSiteId;
}
// documentation inherited
public int identifySite (HttpServletRequest req)
{
checkReloadSites();
String serverName = req.getServerName();
// scan for the mapping that matches the specified domain
int msize = _mappings.size();
for (int i = 0; i < msize; i++) {
SiteMapping mapping = _mappings.get(i);
if (serverName.endsWith(mapping.domain)) {
return mapping.siteId;
}
}
// if we matched nothing, return the default id
return _defaultSiteId;
}
// documentation inherited
public String getSiteString (int siteId)
{
checkReloadSites();
Site site = _sitesById.get(siteId);
if (site == null) {
site = _sitesById.get(_defaultSiteId);
}
return (site == null) ? DEFAULT_SITE_STRING : site.siteString;
}
// documentation inherited
public int getSiteId (String siteString)
{
checkReloadSites();
Site site = _sitesByString.get(siteString);
return (site == null) ? _defaultSiteId : site.siteId;
}
// documentation inherited from interface
public Iterator<Site> enumerateSites ()
{
checkReloadSites();
return _sitesById.values().iterator();
}
/**
* Insert a new site into the site table and into this mapping.
*/
public Site insertNewSite (String siteString)
throws PersistenceException
{
if (_sitesByString.containsKey(siteString)) {
return null;
}
// add it to the db
Site site = new Site();
site.siteString = siteString;
_repo.insertNewSite(site);
// add it to our two mapping tables, taking care to avoid causing enumerateSites() to choke
@SuppressWarnings("unchecked") HashMap<String,Site> newStrings =
(HashMap<String,Site>)_sitesByString.clone();
HashIntMap<Site> newIds = _sitesById.clone();
newIds.put(site.siteId, site);
newStrings.put(site.siteString, site);
_sitesByString = newStrings;
_sitesById = newIds;
return site;
}
/**
* Checks to see if we should reload our sites information from the sites table.
*/
protected void checkReloadSites ()
{
long now = System.currentTimeMillis();
boolean reload = false;
synchronized (this) {
reload = (now - _lastReload > RELOAD_INTERVAL);
if (reload) {
_lastReload = now;
}
}
if (reload) {
try {
_repo.refreshSiteData();
} catch (PersistenceException pe) {
log.warning("Error refreshing site data.", pe);
}
}
}
/**
* Used to load information from the site database.
*/
protected class SiteIdentifierRepository extends SimpleRepository
implements SimpleRepository.Operation<Object>
{
public SiteIdentifierRepository (ConnectionProvider conprov)
{
super(conprov, SITE_IDENTIFIER_IDENT);
}
public void refreshSiteData ()
throws PersistenceException
{
// we are the operation!
execute(this);
}
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
Statement stmt = conn.createStatement();
try {
// first load up the list of sites
String query = "select siteId, siteString from sites";
ResultSet rs = stmt.executeQuery(query);
HashIntMap<Site> sites = new HashIntMap<Site>();
HashMap<String,Site> strings = new HashMap<String,Site>();
while (rs.next()) {
Site site = new Site(rs.getInt(1), rs.getString(2));
sites.put(site.siteId, site);
strings.put(site.siteString, site);
}
_sitesById = sites;
_sitesByString = strings;
// now load up the domain mappings
query = "select domain, siteId from domains";
rs = stmt.executeQuery(query);
ArrayList<SiteMapping> mappings = new ArrayList<SiteMapping>();
while (rs.next()) {
mappings.add(new SiteMapping(rs.getInt(2), rs.getString(1)));
}
// sort the mappings in order of specificity
Collections.sort(mappings, SiteMapping.BY_SPECIFICITY);
_mappings = mappings;
// Log.info("Loaded site mappings " + StringUtil.toString(_mappings) + ".");
// nothing to return
return null;
} finally {
JDBCUtil.close(stmt);
}
}
/**
* Add a new site to the database.
*/
public void insertNewSite (final Site site)
throws PersistenceException
{
executeUpdate(new Operation<Object>() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement("insert into sites (siteString) VALUES (?)");
stmt.setString(1, site.siteString);
if (1 != stmt.executeUpdate()) {
throw new PersistenceException("Not inserted " + site);
}
site.siteId = liaison.lastInsertedId(conn, "sites", "siteId");
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
}
/**
* Used to track domain to site identifier mappings.
*/
protected static class SiteMapping
{
/**
* Sorts site mappings from most specific (www.yahoo.com) to least specific (yahoo.com).
*/
public static final Comparator<SiteMapping> BY_SPECIFICITY = new Comparator<SiteMapping>() {
public int compare (SiteMapping one, SiteMapping two) {
return one._rdomain.compareTo(two._rdomain);
}
};
/** The domain to match. */
public String domain;
/** The site identifier for the associated domain. */
public int siteId;
public SiteMapping (int siteId, String domain) {
this.siteId = siteId;
this.domain = domain;
byte[] bytes = domain.getBytes();
ArrayUtil.reverse(bytes);
_rdomain = new String(bytes);
}
@Override // from Object
public String toString () {
return "[" + domain + " => " + siteId + "]";
}
protected String _rdomain;
}
/** The repository through which we load up site identifier information. */
protected SiteIdentifierRepository _repo;
/** The site id to return if we cannot identify the site from our table data. */
protected int _defaultSiteId;
/** The list of domain to site identifier mappings ordered from most specific domain to least
* specific. */
protected volatile ArrayList<SiteMapping> _mappings = new ArrayList<SiteMapping>();
/** The mapping from integer site identifiers to string site identifiers. */
protected volatile HashIntMap<Site> _sitesById = new HashIntMap<Site>();
/** The mapping from string site identifiers to integer site identifiers. */
protected volatile HashMap<String,Site> _sitesByString = new HashMap<String,Site>();
/** Used to periodically reload our site data. */
protected long _lastReload;
/** Reload our site data every 15 minutes. */
protected static final long RELOAD_INTERVAL = 15 * 60 * 1000L;
}
@@ -0,0 +1,336 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.*;
import javax.servlet.http.HttpServletRequest;
import com.samskivert.text.MessageUtil;
import com.samskivert.util.StringUtil;
import static com.samskivert.Log.log;
/**
* The message manager handles the translation messages for a web application. The webapp should
* construct the message manager with the name of its message properties file and it can then make
* use of the message manager to generate locale specific messages for a request.
*/
public class MessageManager
{
/**
* Constructs a message manager with the specified bundle path. The message manager will be
* instantiating a <code>ResourceBundle</code> with the supplied path, so it should conform to
* the naming conventions defined by that class.
*
* @param siteIdent an optional site identifier that can be used to identify the site via which
* an http request was made.
*
* @see java.util.ResourceBundle
*/
public MessageManager (String bundlePath, Locale deflocale, SiteIdentifier siteIdent)
{
// keep these for later
_bundlePath = bundlePath;
_deflocale = deflocale;
_siteIdent = siteIdent;
}
/**
* If the message manager is to be used in a multi-site environment, it can be configured to
* load site-specific message resources in addition to the default application message
* resources. It must be configured with the facilities to load site-specific resources by a
* call to this function.
*
* @param siteBundlePath the path to the site-specific message resources.
* @param siteLoader a site-specific resource loader, properly configured with a site
* identifier.
*/
public void activateSiteSpecificMessages (String siteBundlePath, SiteResourceLoader siteLoader)
{
_siteBundlePath = siteBundlePath;
_siteLoader = siteLoader;
}
/**
* Return true if the specifed path exists in the resource bundle.
*/
public boolean exists (HttpServletRequest req, String path)
{
return (getMessage(req, path, false) != null);
}
/**
* Looks up the message with the specified path in the resource bundle most appropriate for the
* locales described as preferred by the request. Always reports missing paths.
*/
public String getMessage (HttpServletRequest req, String path)
{
return getMessage(req, path, true);
}
/**
* Looks up the message with the specified path in the resource bundle most appropriate for the
* locales described as preferred by the request, then substitutes the supplied arguments into
* that message using a <code>MessageFormat</code> object.
*
* @see java.text.MessageFormat
*/
public String getMessage (HttpServletRequest req, String path, Object[] args)
{
String msg = getMessage(req, path, true);
// we may cache message formatters later, but for now just use the static convenience
// function
return MessageFormat.format(MessageUtil.escape(msg), args);
}
/**
* Looks up the message with the specified path in the resource bundle most appropriate for the
* locales described as preferred by the request. If requested it will log a missing path and
* return the path as the translation (which should make it obvious in the servlet that the
* translation is missing) otherwise it returns null.
*/
protected String getMessage (HttpServletRequest req, String path, boolean reportMissing)
{
if (path == null) {
return "[null message key]";
}
// if the key is tainted, just return the untainted key
if (MessageUtil.isTainted(path)) {
return MessageUtil.untaint(path);
}
// attempt to determine whether or not this is a compound key
int tidx = path.indexOf("|");
if (tidx != -1) {
String key = path.substring(0, tidx);
String argstr = path.substring(tidx+1);
String[] args = StringUtil.split(argstr, "|");
// unescape and translate the arguments
for (int ii = 0; ii < args.length; ii++) {
// if the argument is tainted, do no further translation (it might contain |s or
// other fun stuff)
if (MessageUtil.isTainted(args[ii])) {
args[ii] = MessageUtil.unescape(MessageUtil.untaint(args[ii]));
} else {
args[ii] = getMessage(req, MessageUtil.unescape(args[ii]));
}
}
return getMessage(req, key, args);
}
// load up the matching resource bundles (the array will contain the site-specific
// resources first and the application resources second); use the locale preferred by the
// client if possible
ResourceBundle[] bundles = resolveBundles(req);
String message = null;
if (bundles != null) {
int blength = bundles.length;
for (int i = 0; message == null && i < blength; i++) {
try {
if (bundles[i] != null) {
message = bundles[i].getString(path);
}
} catch (MissingResourceException mre) {
// no complaints, just try the bundle in the enclosing scope
}
}
}
// if we found a message, check it for embedded message links
if (message != null) {
int oidx = -1;
while ((oidx = message.indexOf("{", oidx+1)) != -1) {
int cidx = message.indexOf("}", oidx+1);
if (cidx == -1) {
// something's funny, just stop fiddling
break;
}
String ref = message.substring(oidx+1, cidx);
// avoid trivial infinite recursion
if (ref.equals(path)) {
throw new IllegalStateException(
"Illegal self-referential message " + path + " = " + message + ".");
}
if (ref.length() > 0 && !Character.isDigit(ref.charAt(0))) {
String refmsg = getMessage(req, ref, true);
message = message.substring(0, oidx) + refmsg + message.substring(cidx+1);
oidx += refmsg.length();
}
}
return message;
}
if (reportMissing) {
// if there's no translation for this path, complain about it
log.warning("Missing translation message", "path", path, "url", getURL(req));
return path;
}
return null;
}
/**
* Finds the closest matching resource bundle for the locales specified as preferred by the
* client in the supplied http request.
*/
protected ResourceBundle[] resolveBundles (HttpServletRequest req)
{
// first look to see if we've cached the bundles for this request in the request object
ResourceBundle[] bundles = null;
if (req != null) {
bundles = (ResourceBundle[])req.getAttribute(getBundleCacheName());
}
if (bundles != null) {
return bundles;
}
ClassLoader siteLoader = null;
String siteString = null;
if (_siteIdent != null) {
int siteId = _siteIdent.identifySite(req);
siteString = _siteIdent.getSiteString(siteId);
// grab our site-specific class loader if we have one
if (_siteLoader != null) {
try {
siteLoader = _siteLoader.getSiteClassLoader(siteId);
} catch (IOException ioe) {
log.warning("Unable to fetch site-specific classloader", "siteId", siteId,
"error", ioe);
}
}
}
// try looking up the appropriate bundles
bundles = new ResourceBundle[2];
// first from the site-specific classloader
if (siteLoader != null) {
bundles[0] = resolveBundle(req, _siteBundlePath, siteLoader, false);
} else if (siteString != null) {
// or just try prefixing the site string to the normal bundle path and see if that
// turns something up
bundles[0] = resolveBundle(req, siteString + "_" + _bundlePath,
getClass().getClassLoader(), true);
}
// then from the default classloader
bundles[1] = resolveBundle(req, _bundlePath, getClass().getClassLoader(), false);
// if we found either or both bundles, cache 'em
if (bundles[0] != null || bundles[1] != null && req != null) {
req.setAttribute(getBundleCacheName(), bundles);
}
return bundles;
}
/**
* Resolves the default resource bundle based on the locale information provided in the
* supplied http request object.
*/
protected ResourceBundle resolveBundle (HttpServletRequest req, String bundlePath,
ClassLoader loader, boolean silent)
{
ResourceBundle bundle = null;
if (req != null) {
Enumeration<?> locales = req.getLocales();
while (locales.hasMoreElements()) {
Locale locale = (Locale)locales.nextElement();
try {
// java caches resource bundles, so we don't need to reinvent the wheel here.
// however, java also falls back from a specific bundle to a more general one
// if it can't find a specific bundle. that's real nice of it, but we want
// first to see whether or not we have exact matches on any of the preferred
// locales specified by the client. if we don't, then we can rely on java's
// fallback mechanisms
bundle = ResourceBundle.getBundle(bundlePath, locale, loader);
// if it's an exact match, off we go
if (bundle.getLocale().equals(locale)) {
break;
}
} catch (MissingResourceException mre) {
// no need to freak out quite yet, see if we have something for one of the
// other preferred locales
}
}
}
// if we were unable to find an exact match for any of the user's preferred locales, take
// their most preferred and let java perform it's fallback logic on that one
if (bundle == null) {
Locale locale = (req == null) ? _deflocale : req.getLocale();
try {
bundle = ResourceBundle.getBundle(bundlePath, locale, loader);
} catch (MissingResourceException mre) {
if (!silent) {
// if we were unable even to find a default bundle, we may want to log a
// warning
log.warning("Unable to resolve any message bundle", "req", getURL(req),
"locale", locale, "bundlePath", bundlePath, "classLoader", loader,
"siteBundlePath", _siteBundlePath, "siteLoader", _siteLoader);
}
}
}
return bundle;
}
/** Helper function. */
protected String getURL (HttpServletRequest req)
{
return (req == null) ? "<none>" : req.getRequestURL().toString();
}
/** Used to cache resolved bundles in a request. */
protected String getBundleCacheName ()
{
return BUNDLE_CACHE_PREFIX + _bundlePath;
}
/** The path, relative to the classpath, to our resource bundles. */
protected String _bundlePath;
/** The path to the site-specific message bundles, fetched via the site-specific resource
* loader. */
protected String _siteBundlePath;
/** The resource loader with which to fetch our site-specific message bundles. */
protected SiteResourceLoader _siteLoader;
/** The site identifier we use to determine through which site a request was made. */
protected SiteIdentifier _siteIdent;
/** The locale to use if we are accessed without an HTTP request. */
protected Locale _deflocale;
/** The attribute name that we use for caching resource bundles in request objects. */
protected static final String BUNDLE_CACHE_PREFIX =
"com.samskivert.servlet.MessageManager:CachedResourceBundle:";
}
@@ -0,0 +1,42 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet;
/**
* A redirect exception is thrown by servlet services when they require
* that the user be redirected to a different URL rather than continue
* processing this request. It is expected that redirect handling can be
* implemented in a single place such that servlets can simply allow this
* exception to propagate up to the proper handler which will then issue
* the appropriate redirect header.
*/
public class RedirectException extends Exception
{
public RedirectException (String url)
{
super(url);
}
public String getRedirectURL ()
{
return getMessage();
}
}
@@ -0,0 +1,57 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet;
import com.samskivert.util.StringUtil;
/**
* Represents a site mapping known to a {@link SiteIdentifier}.
*
* @see SiteIdentifier#enumerateSites
*/
public class Site
{
/** The site's unqiue identifier. */
public int siteId;
/** The site's human readable identifier (i.e., "monkeybutter"). */
public String siteString;
/** Constructs a site record with the specified id and string. */
public Site (int siteId, String siteString)
{
this.siteId = siteId;
this.siteString = siteString;
}
/**
* Constructs a blank record for unserialization from the repository.
*/
public Site ()
{
}
@Override
public String toString ()
{
return StringUtil.fieldsToString(this);
}
}
@@ -0,0 +1,81 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet;
import java.util.Iterator;
import javax.servlet.http.HttpServletRequest;
/**
* Responsible for determining the unique site identifier based on information available in the
* HTTP request. Site identifiers are integers ranging from 1 to {@link Integer#MAX_VALUE}. Because
* the site identifier implementation is likely to have access to the site classification metadata,
* this interface is also used to map integer site identifiers to string site identifiers.
*/
public interface SiteIdentifier
{
/** The default site identifier, to be used when a site cannot be identified or for site
* identifiers that don't wish to distinguish between sites. */
public static final int DEFAULT_SITE_ID = -1;
/** The string identifier for the default site. */
public static final String DEFAULT_SITE_STRING = "default";
/**
* Returns the unique identifier for the site on which this request originated. That may be
* divined by looking at the server name, or perhaps a request parameter, or part of the path
* info. The mechanism (or mechanisms) are up to the implementation.
*
* @param req the http servlet request the site for which we are trying to identify.
*
* @return the unique site identifier requestsed or {@link #DEFAULT_SITE_ID} if the site could
* not be identified. No site should ever have a site id value of 0.
*/
public int identifySite (HttpServletRequest req);
/**
* Returns a string representation of the site identifier. The SiteIdentifier in use can map
* the site ids to strings however it likes as long as it consistently maps the same identifier
* to the same string and vice versa. Presumably these strings would be human readable and
* meaningful.
*
* @param siteId the unique integer identifier for the site that we wish to be identified by a
* string.
*
* @return the string identifier for this site.
*/
public String getSiteString (int siteId);
/**
* Returns the site identifier for the site associated with the supplied site string. The
* SiteIdentifier in use can map the site ids to strings however it likes as long as it
* consistently maps the same string to the same identifier and vice versa.
*
* @param siteString the string to be converted into a site identifer.
*
* @return the integer identifier for this site.
*/
public int getSiteId (String siteString);
/**
* Returns an enumerator over all {@link Site} mappings known to this SiteIdentifier.
*/
public Iterator<Site> enumerateSites ();
}
@@ -0,0 +1,384 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.servlet.http.HttpServletRequest;
import com.samskivert.util.HashIntMap;
import static com.samskivert.Log.log;
/**
* Web applications may wish to load resources in such a way that the site
* on which they are running is allowed to override a resource with a
* site-specific version (a header, footer or navigation template for
* example). The site resource loader provides this capability by loading
* resources from a site-specific jar file.
*
* <p> The site resource loader must be configured with the path to the
* site-specific jar files, the names of which are dictated by the string
* identifiers returned by the {@link SiteIdentifier} provided to the
* resource loader at construct time. For example, if the configuration
* dictates that site-specific jar files are located in
* <code>/usr/share/java/webapps/site-data</code> and the site identifier
* returns <code>samskivert</code> as the site identifier for a particular
* request, site-specific resources will be loaded from
* <code>/usr/share/java/webapps/site-data/samskivert.jar</code>.
*/
public class SiteResourceLoader
{
/**
* Constructs a new resource loader.
*
* @param siteIdent the site identifier to be used to identify which
* site through which a request was made when loading resources.
* @param siteJarPath the path to the site-specific jar files.
*/
public SiteResourceLoader (
SiteIdentifier siteIdent, String siteJarPath)
{
// keep this stuff around
_siteIdent = siteIdent;
_jarPath = siteJarPath;
}
/**
* Loads the specific resource, from the site-specific jar file if one
* exists and contains the specified resource. If no resource exists
* with that path, null will be returned.
*
* @param req the http request for which we are loading a resource
* (this will be used to determine for which site the resource will be
* loaded).
* @param path the path to the desired resource.
*
* @return an input stream via which the resource can be read or null
* if no resource could be located with the specified path.
*
* @exception IOException thrown if an I/O error occurs while loading
* a resource.
*/
public InputStream getResourceAsStream (
HttpServletRequest req, String path)
throws IOException
{
return getResourceAsStream(_siteIdent.identifySite(req), path);
}
/**
* Loads the specific resource, from the site-specific jar file if one
* exists and contains the specified resource. If no resource exists
* with that path, null will be returned.
*
* @param siteId the unique identifer for the site for which we are
* loading the resource.
* @param path the path to the desired resource.
*
* @return an input stream via which the resource can be read or null
* if no resource could be located with the specified path.
*
* @exception IOException thrown if an I/O error occurs while loading
* a resource.
*/
public InputStream getResourceAsStream (int siteId, String path)
throws IOException
{
// Log.info("Loading site resource [siteId=" + siteId +
// ", path=" + path + "].");
// synchronize on the lock to ensure that only one thread per site
// is concurrently executing
synchronized (getLock(siteId)) {
SiteResourceBundle bundle = getBundle(siteId);
// make sure the path has no leading slash
if (path.startsWith("/")) {
path = path.substring(1);
}
// obtain our resource from the bundle
return bundle.getResourceAsStream(path);
}
}
/**
* Returns the last modification time of the site-specific jar file
* for the specified site.
*
* @exception IOException thrown if an error occurs accessing the
* site-specific jar file (like it doesn't exist).
*/
public long getLastModified (int siteId)
throws IOException
{
// synchronize on the lock to ensure that only one thread per site
// is concurrently executing
synchronized (getLock(siteId)) {
return getBundle(siteId).getLastModified();
}
}
/**
* Returns a class loader that loads resources from the site-specific
* jar file for the specified site. If no site-specific jar file
* exists for the specified site, null will be returned.
*/
public ClassLoader getSiteClassLoader (int siteId)
throws IOException
{
// synchronize on the lock to ensure that only one thread per site
// is concurrently executing
synchronized (getLock(siteId)) {
// see if we've already got one
ClassLoader loader = _loaders.get(siteId);
// create one if we've not
if (loader == null) {
final SiteResourceBundle bundle = getBundle(siteId);
if (bundle == null) {
// no bundle... no classloader.
return null;
}
loader = AccessController.doPrivileged(new PrivilegedAction<SiteClassLoader>() {
public SiteClassLoader run () {
return new SiteClassLoader(bundle);
}
});
_loaders.put(siteId, loader);
}
return loader;
}
}
@Override
public String toString ()
{
return "[jarPath=" + _jarPath + "]";
}
/**
* We synchronize on a per-site basis, but we use a separate lock
* object for each site so that the process of loading a bundle for
* the first time does not require blocking access to resources from
* other sites.
*/
protected Object getLock (int siteId)
{
Object lock = null;
synchronized (_locks) {
lock = _locks.get(siteId);
// create a lock object if we haven't one already
if (lock == null) {
_locks.put(siteId, lock = new Object());
}
}
return lock;
}
/**
* Obtains the site-specific jar file for the specified site. This
* should only be called when the lock for this site is held.
*/
protected SiteResourceBundle getBundle (int siteId)
throws IOException
{
// look up the site resource bundle for this site
SiteResourceBundle bundle = _bundles.get(siteId);
// if we haven't got one, create one
if (bundle == null) {
// obtain the string identifier for this site
String ident = _siteIdent.getSiteString(siteId);
// compose that with the jar file directory to obtain the
// path to the site-specific jar file
File file = new File(_jarPath, ident + JAR_EXTENSION);
// create a handle for this site-specific jar file
bundle = new SiteResourceBundle(file);
// cache our new bundle
_bundles.put(siteId, bundle);
}
return bundle;
}
/**
* Encapsulates the information we need to load data from a site
* resource bundle as well as to determine whether the loaded bundle
* is up to date.
*/
public static class SiteResourceBundle
{
/** The object through which we load resources from the
* site-specific jar file. */
public JarFile jarFile;
/** A handle on the site-specific jar file. */
public File file;
/**
* Constructs a new site resource bundle. The associated jar file
* will be opened the first time a resource is read.
*/
public SiteResourceBundle (File file)
throws IOException
{
this.file = file;
}
/**
* Fetches the specified resource from our site-specific jar file.
* The last modified time of the underlying jar file may be
* checked to determine whether or not it needs to be reloaded.
*
* @return an input stream via which the resource can be read or
* null if no resource exists with the specified path.
*/
public InputStream getResourceAsStream (String path)
throws IOException
{
// open or reopen our underlying jar file as necessary
refreshJarFile();
// now load up the resource
JarEntry entry = jarFile.getJarEntry(path);
return (entry == null) ? null : jarFile.getInputStream(entry);
}
/**
* Returns the last modified time of the underlying jar file.
*/
public long getLastModified ()
throws IOException
{
// open or reopen our underlying jar file as necessary
refreshJarFile();
return _lastModified;
}
@Override public String toString ()
{
return "[bundle=" + file + "]";
}
/**
* Reopens our site-specific jar file if it has been modified
* since it was last opened.
*/
protected void refreshJarFile ()
throws IOException
{
// ensure that the file exists
if (!file.exists()) {
String errmsg = "No site-specific jar file " +
"[path=" + file.getPath() + "].";
throw new FileNotFoundException(errmsg);
}
// determine whether or not we need to create a new jarfile
// instance
if (file.lastModified() > _lastModified) {
// make a note of the last modified time
_lastModified = file.lastModified();
// close our old jar file if we've got one
if (jarFile != null) {
jarFile.close();
}
log.info("Opened site bundle", "path", file.getPath());
// and open a new one
jarFile = new JarFile(file);
}
}
/** The last modified time of the jar file at the time that we
* opened it for reading. */
protected long _lastModified;
}
protected static class SiteClassLoader extends ClassLoader
{
public SiteClassLoader (SiteResourceBundle bundle)
{
_bundle = bundle;
}
@Override public InputStream getResourceAsStream (String path)
{
try {
return _bundle.getResourceAsStream(path);
} catch (IOException ioe) {
log.warning("Error loading resource from jarfile", "bundle", _bundle, "path", path,
"error", ioe);
return null;
}
}
@Override public String toString ()
{
return _bundle.toString();
}
protected SiteResourceBundle _bundle;
}
/** The site identifier we use to identify requests. */
protected SiteIdentifier _siteIdent;
/** The path to our site-specific jar files. */
protected String _jarPath;
/** We synchronize on a per-site basis. */
protected HashIntMap<Object> _locks = new HashIntMap<Object>();
/** The table of site-specific jar file information. */
protected HashIntMap<SiteResourceBundle> _bundles =
new HashIntMap<SiteResourceBundle>();
/** The table of site-specific class loaders. */
protected HashIntMap<ClassLoader> _loaders = new HashIntMap<ClassLoader>();
/** The default path to the site-specific jar files. This won't be
* used without logging a complaint first. */
protected static final String DEFAULT_SITE_JAR_PATH =
"/usr/share/java/webapps/site-data";
/** The file extension to be appended to the string site identifier to
* obtain the file name of the site-specific jar file. */
protected static final String JAR_EXTENSION = ".jar";
}
@@ -0,0 +1,33 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
$Id: package.html,v 1.1 2001/08/12 01:34:31 mdb Exp $
samskivert library - useful routines for java programs
Copyright (C) 2001 Michael Bayne
This library 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 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-->
</head>
<body bgcolor="white">
Provides services and patterns that are useful in developing web
applications.
</body>
</html>
@@ -0,0 +1,32 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.user;
/**
* Thrown when a user authentication attempt failed.
*/
public class AuthenticationFailedException extends Exception
{
public AuthenticationFailedException (String message)
{
super(message);
}
}
@@ -0,0 +1,46 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.user;
/**
* Provides a means for applications to use their own application-specific
* authentication schemes for validating a user by constructing their own
* authenticator and passing it to {@link UserManager#login}.
*/
public interface Authenticator
{
/**
* Checks whether the user should be authenticated based on the
* supplied user record and the specified user information. Throws an
* {@link AuthenticationFailedException} if the user fails to pass the
* authentication check for some reason.
*
* @param user the definitive user record loaded from the persistent
* repository against which the user-supplied data is to be checked.
* @param username the username supplied by the user.
* @param password the plaintext password supplied by the user.
*
* @throws AuthenticationFailedException if the user failed to pass
* the authentication check.
*/
public void authenticateUser (User user, String username, Password password)
throws AuthenticationFailedException;
}
@@ -0,0 +1,32 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.user;
/**
* Thrown during authentication when an invalid password is supplied.
*/
public class InvalidPasswordException extends AuthenticationFailedException
{
public InvalidPasswordException (String message)
{
super(message);
}
}
@@ -0,0 +1,32 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.user;
/**
* Thrown during user account creation if an invalid username is supplied.
*/
public class InvalidUsernameException extends Exception
{
public InvalidUsernameException (String message)
{
super(message);
}
}
@@ -0,0 +1,32 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.user;
/**
* Thrown when a user cannot be located in the user database.
*/
public class NoSuchUserException extends AuthenticationFailedException
{
public NoSuchUserException (String message)
{
super(message);
}
}
@@ -0,0 +1,73 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.user;
/**
* Represents an encrypted password. Currently only used when creating
* user accounts.
*/
public class Password
{
/**
* Returns the clear password text. This method may return null if
* none was provided when creating the password.
*/
public String getCleartext ()
{
return _cleartext;
}
/**
* Returns the encrypted password text.
*/
public String getEncrypted ()
{
return _encrypted;
}
/**
* Creates a password instance from the supplied plaintext.
*/
public static Password makeFromClear (String password)
{
return new Password(password, UserUtil.encryptPassword(password));
}
/**
* Creates a password instance from the supplied already encrypted
* text. <em>Note:</em> the encrypted text must be obtained from
* {@link UserUtil#encryptPassword}.
*/
public static Password makeFromCrypto (String encrypted)
{
return new Password(null, encrypted);
}
/** Creates a password instance. */
protected Password (String cleartext, String encrypted)
{
_cleartext = cleartext;
_encrypted = encrypted;
}
protected String _cleartext;
protected String _encrypted;
}
@@ -0,0 +1,180 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.user;
import java.sql.Date;
import com.samskivert.jdbc.jora.FieldMask;
import com.samskivert.util.StringUtil;
/**
* A user object contains information about a registered user in our web application environment.
* Users are stored in the user repository (a database table) and loaded during a request based on
* authentication information provided with the request headers.
*
* <p><b>Note:</b> Do not modify any of the fields of this object directly. Use the
* <code>set</code> methods to make updates. If no set methods exist, you shouldn't be modifying
* that field.
*/
public class User
{
/** The user's assigned integer userid. */
public int userId;
/** The user's chosen username. */
public String username;
/** The date this record was created. */
public Date created;
/** The user's real name (first, last and whatever else they opt to provide). */
public String realname;
/** The user's chosen password (encrypted). */
public String password;
/** The user's email address. */
public String email;
/** The site identifier of the site through which the user created their account. (Their
* affiliation, if you will.) */
public int siteId;
/**
* Updates the user's username.
*/
public void setUsername (Username username)
{
this.username = username.getUsername();
setModified("username");
}
/**
* Updates the user's real name.
*/
public void setRealName (String realname)
{
this.realname = realname;
setModified("realname");
}
/**
* Updates the user's password.
*
* @param password The user's new (unencrypted) password.
*/
public void setPassword (String password)
{
setPassword(Password.makeFromClear(password));
}
/**
* Updates the user's password.
*
* @param password The user's new password.
*/
public void setPassword (Password password)
{
this.password = password.getEncrypted();
setModified("password");
}
/**
* Updates the user's email address.
*/
public void setEmail (String email)
{
this.email = email;
setModified("email");
}
/**
* Updates the user's site id.
*/
public void setSiteId (int siteId)
{
this.siteId = siteId;
setModified("siteId");
}
/**
* Compares the supplied password with the password associated with this user record.
*
* @return true if the passwords match, false if they do not.
*/
public boolean passwordsMatch (Password password)
{
return this.password.equals(password.getEncrypted());
}
/**
* @return true if this User has been "deleted".
*/
public boolean isDeleted ()
{
// a deleted account has an "=" in the username
return (-1 != username.indexOf('='));
}
/**
* Returns true if this user is an admin, false otherwise. The default implementation does not
* track admin status and always returns false.
*/
public boolean isAdmin ()
{
return false;
}
@Override // from Object
public String toString ()
{
return StringUtil.fieldsToString(this);
}
/**
* Called by the repository to find out which fields have been modified since the object was
* loaded.
*/
protected FieldMask getDirtyMask ()
{
return _dirty;
}
/**
* Called by the repository to configure this user record with a field mask that it can use to
* track modified fields.
*/
protected void setDirtyMask (FieldMask dirty)
{
_dirty = dirty;
}
/**
* Marks the supplied field as dirty.
*/
protected void setModified (String field)
{
_dirty.setModified(field);
}
/** Our dirty field mask. */
protected transient FieldMask _dirty;
}
@@ -0,0 +1,35 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.user;
import com.samskivert.io.PersistenceException;
/**
* Thrown during user account creation when a user with the requested
* username already exists.
*/
public class UserExistsException extends PersistenceException
{
public UserExistsException (String message)
{
super(message);
}
}
@@ -0,0 +1,401 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.user;
import java.util.Properties;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.servlet.RedirectException;
import com.samskivert.servlet.util.CookieUtil;
import com.samskivert.servlet.util.RequestUtils;
import com.samskivert.util.Interval;
import com.samskivert.util.RunQueue;
import com.samskivert.util.StringUtil;
import com.samskivert.util.Tuple;
import static com.samskivert.Log.log;
/**
* The user manager provides easy access to user objects for servlets. It takes care of cookie
* management involved in login, logout and loading a user record during an authenticated session.
*/
public class UserManager
{
/** An instance of the insecure authenticator for general-purpose use. */
public static final Authenticator AUTH_INSECURE = new InsecureAuthenticator();
/** An instance of the password authenticator for general-purpose use. */
public static final Authenticator AUTH_PASSWORD = new PasswordAuthenticator();
/**
* A totally insecure authenticator that authenticates any user. <em>Note:</em> Applications
* that make use of this authenticator should make sure the user has already been authenticated
* through some other means.
*/
public static class InsecureAuthenticator implements Authenticator
{
// documentation inherited
public void authenticateUser (User user, String username, Password password)
throws InvalidPasswordException
{
// don't care
}
}
/**
* An authenticator that requires that the user-supplied password match the actual user
* password.
*/
public static class PasswordAuthenticator implements Authenticator
{
// documentation inherited
public void authenticateUser (User user, String username, Password password)
throws AuthenticationFailedException
{
if (!user.passwordsMatch(password)) {
throw new InvalidPasswordException("error.invalid_password");
}
}
}
/**
* Prepares this user manager for operation. Presently the user manager requires the
* following configuration information:
*
* <ul>
* <li><code>login_url</code>: Should be set to the URL to which to redirect a requester if
* they are required to login before accessing the requested page. For example:
*
* <pre>
* login_url = /usermgmt/login.ajsp?return=%R
* </pre>
*
* The <code>%R</code> will be replaced with the URL encoded URL the user is currently
* requesting (complete with query parameters) so that the login code can redirect the user
* back to this request once they are authenticated.
* </ul>
*
* @param config the user manager configuration properties.
* @param conprov the database connection provider that will be used to obtain a connection to
* the user database.
*/
public void init (Properties config, ConnectionProvider conprov)
throws PersistenceException
{
init(config, conprov, null);
}
/**
* Prepares this user manager for operation. See {@link #init(Properties,ConnectionProvider)}.
*
* @param pruneQueue an optional run queue on which to run our periodic session pruning task.
*/
public void init (Properties config, ConnectionProvider conprov, RunQueue pruneQueue)
throws PersistenceException
{
// save this for later
_config = config;
// create the user repository
_repository = createRepository(conprov);
// fetch the login URL from the properties
_loginURL = config.getProperty("login_url");
if (_loginURL == null) {
log.warning("No login_url supplied in user manager config. Authentication won't work.");
_loginURL = "/missing_login_url";
}
// look up any override to our user auth cookie
String authCook = config.getProperty("auth_cookie.name");
if (!StringUtil.isBlank(authCook)) {
_userAuthCookie = authCook;
}
if (USERMGR_DEBUG) {
log.info("UserManager initialized", "acook", _userAuthCookie, "login", _loginURL);
}
// register a cron job to prune the session table every hour
_pruner = new Interval() {
@Override public void expired () {
try {
_repository.pruneSessions();
} catch (PersistenceException pe) {
log.warning("Error pruning session table.", pe);
}
}
};
if (pruneQueue != null) {
_pruner.setRunQueue(pruneQueue);
}
_pruner.schedule(SESSION_PRUNE_INTERVAL, true);
}
public void shutdown ()
{
// cancel our session table pruning thread
_pruner.cancel();
}
/**
* Returns a reference to the repository in use by this user manager.
*/
public UserRepository getRepository ()
{
return _repository;
}
/**
* Fetches the necessary authentication information from the http request and loads the user
* identified by that information.
*
* @return the user associated with the request or null if no user was associated with the
* request or if the authentication information is bogus.
*/
public User loadUser (HttpServletRequest req)
throws PersistenceException
{
String authcook = CookieUtil.getCookieValue(req, _userAuthCookie);
if (USERMGR_DEBUG) {
log.info("Loading user by cookie", _userAuthCookie, authcook);
}
return loadUser(authcook);
}
/**
* Loads up a user based on the supplied session authentication token.
*/
public User loadUser (String authcode)
throws PersistenceException
{
User user = (authcode == null) ? null : _repository.loadUserBySession(authcode);
if (USERMGR_DEBUG) {
log.info("Loaded user by authcode", "code", authcode, "user", user);
}
return user;
}
/**
* Fetches the necessary authentication information from the http request and loads the user
* identified by that information. If no user could be loaded (because the requester is not
* authenticated), a redirect exception will be thrown to redirect the user to the login page
* specified in the user manager configuration.
*
* @return the user associated with the request.
*/
public User requireUser (HttpServletRequest req)
throws PersistenceException, RedirectException
{
User user = loadUser(req);
// if no user was loaded, we need to redirect these fine people to the login page
if (user == null) {
// first construct the redirect URL
String eurl = RequestUtils.getLocationEncoded(req);
String target = _loginURL.replace("%R", eurl);
if (USERMGR_DEBUG) {
log.info("No user found in require, redirecting", "to", target);
}
throw new RedirectException(target);
}
return user;
}
/**
* Attempts to authenticate the requester and initiate an authenticated session for them. An
* authenticated session involves their receiving a cookie that proves them to be authenticated
* and an entry in the session database being created that maps their information to their
* userid. If this call completes, the session was established and the proper cookies were set
* in the supplied response object. If invalid authentication information is provided or some
* other error occurs, an exception will be thrown.
*
* @param username The username supplied by the user.
* @param password The password supplied by the user.
* @param persist If true, the cookie will expire in one month, if false, the cookie will
* expire at the end of the user's browser session.
* @param req The request via which the login page was loaded.
* @param rsp The response in which the cookie is to be set.
* @param auth The authenticator used to check whether the user should be authenticated.
*
* @return the user object of the authenticated user.
*/
public User login (String username, Password password, boolean persist,
HttpServletRequest req, HttpServletResponse rsp, Authenticator auth)
throws PersistenceException, AuthenticationFailedException
{
// load up the requested user
User user = _repository.loadUser(username);
if (user == null) {
throw new NoSuchUserException("error.no_such_user");
}
// run the user through the authentication gamut
auth.authenticateUser(user, username, password);
// give them the necessary cookies and business
effectLogin(user, persist ? PERSIST_EXPIRE_DAYS : NON_PERSIST_EXPIRE_DAYS, req, rsp);
return user;
}
/**
* Attempts to authenticate the requester and initiate an authenticated session for them. A
* session token will be assigned to the user and returned along with the associated {@link
* User} record. It is assumed that the client will maintain the session token via its own
* means.
*
* @param username the username supplied by the user.
* @param password the password supplied by the user.
* @param expires the number of days in which this session should expire.
* @param auth the authenticator used to check whether the user should be authenticated.
*
* @return the user object of the authenticated user.
*/
public Tuple<User,String> login (
String username, Password password, int expires, Authenticator auth)
throws PersistenceException, AuthenticationFailedException
{
// load up the requested user
User user = _repository.loadUser(username);
if (user == null) {
throw new NoSuchUserException("error.no_such_user");
}
// run the user through the authentication gamut
auth.authenticateUser(user, username, password);
// register a session for this user
String authcode = _repository.registerSession(user, expires);
if (USERMGR_DEBUG) {
log.info("Session started", "user", username, "code", authcode);
}
return new Tuple<User,String>(user, authcode);
}
/**
* If a user is already known to be authenticated for one reason or other, this method can be
* used to give them the appropriate authentication cookies to effect their login.
*
* @param expires the number of days in which to expire the session cookie, 0 means expire at
* the end of the browser session.
*/
public void effectLogin (
User user, int expires, HttpServletRequest req, HttpServletResponse rsp)
throws PersistenceException
{
String authcode = _repository.registerSession(user, Math.max(expires, 1));
Cookie acookie = new Cookie(_userAuthCookie, authcode);
// strip the hostname from the server and use that as the domain unless configured not to
if (!"false".equalsIgnoreCase(_config.getProperty("auth_cookie.strip_hostname"))) {
CookieUtil.widenDomain(req, acookie);
}
acookie.setPath("/");
acookie.setMaxAge((expires > 0) ? (expires*24*60*60) : -1);
if (USERMGR_DEBUG) {
log.info("Setting cookie " + acookie + ".");
}
rsp.addCookie(acookie);
}
/**
* Logs the user out.
*/
public void logout (HttpServletRequest req, HttpServletResponse rsp)
{
// nothing to do if they don't already have an auth cookie
String authcode = CookieUtil.getCookieValue(req, _userAuthCookie);
if (authcode == null) {
return;
}
// set them up the bomb
Cookie c = new Cookie(_userAuthCookie, "x");
c.setPath("/");
c.setMaxAge(0);
CookieUtil.widenDomain(req, c);
if (USERMGR_DEBUG) {
log.info("Clearing cookie " + c + ".");
}
rsp.addCookie(c);
// we need an unwidened one to ensure that old-style cookies are wiped as well
c = new Cookie(_userAuthCookie, "x");
c.setPath("/");
c.setMaxAge(0);
rsp.addCookie(c);
}
/**
* Validates that the supplied session key is still valid and if so, refreshes it for the
* specified number of days.
*
* @return true if the session was located and refreshed, false otherwise.
*/
public boolean refreshSession (String sessionKey, int expireDays)
throws PersistenceException
{
return _repository.refreshSession(sessionKey, expireDays);
}
/**
* Called by the user manager to create the user repository. Derived classes can override this
* and create a specialized repository if they so desire.
*/
protected UserRepository createRepository (ConnectionProvider conprov)
throws PersistenceException
{
return new UserRepository(conprov);
}
/** Our user manager configuration. */
protected Properties _config;
/** The user repository. */
protected UserRepository _repository;
/** The interval for user session pruning. */
protected Interval _pruner;
/** The URL for the user login page. */
protected String _loginURL;
/** The name of our user authentication cookie. */
protected String _userAuthCookie = USERAUTH_COOKIE;
/** The user authentication cookie name. */
protected static final String USERAUTH_COOKIE = "id_";
/** Prune the session table every hour. */
protected static final long SESSION_PRUNE_INTERVAL = 60L * 60L * 1000L;
/** Indicates how long (in days) that a "persisting" session token should last. */
protected static final int PERSIST_EXPIRE_DAYS = 30;
/** Indicates how long (in days) that a "non-persisting" session token should last. */
protected static final int NON_PERSIST_EXPIRE_DAYS = 1;
/** Change this to true and recompile to debug cookie handling. */
protected static final boolean USERMGR_DEBUG = false;
}
@@ -0,0 +1,517 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.user;
import java.sql.Connection;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Calendar;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.JORARepository;
import com.samskivert.jdbc.jora.FieldMask;
import com.samskivert.jdbc.jora.Table;
import com.samskivert.util.HashIntMap;
import com.samskivert.util.StringUtil;
import com.samskivert.servlet.SiteIdentifier;
/**
* Interfaces with the RDBMS in which the user information is stored. The user repository
* encapsulates the creating, loading and management of users and sessions.
*/
public class UserRepository extends JORARepository
{
/**
* The database identifier used to obtain a connection from our connection provider. The value
* is <code>userdb</code> which you'll probably need to know to provide the proper
* configuration to your connection provider.
*/
public static final String USER_REPOSITORY_IDENT = "userdb";
/**
* Creates the repository and opens the user database. The database identifier used to fetch
* our database connection is documented by {@link #USER_REPOSITORY_IDENT}.
*
* @param provider the database connection provider.
*/
public UserRepository (ConnectionProvider provider)
{
super(provider, USER_REPOSITORY_IDENT);
}
/**
* Requests that a new user be created in the repository.
*
* @param username the username of the new user to create.
* @param password the password for the new user.
* @param realname the user's real name.
* @param email the user's email address.
* @param siteId the unique identifier of the site through which this account is being
* created. The resulting user will be tracked as originating from this site for accounting
* purposes ({@link SiteIdentifier#DEFAULT_SITE_ID} can be used by systems that don't desire to
* perform site tracking.
*
* @return The userid of the newly created user.
*/
public int createUser (
Username username, Password password, String realname, String email, int siteId)
throws UserExistsException, PersistenceException
{
// create a new user object...
User user = new User();
user.setDirtyMask(_utable.getFieldMask());
// ...configure it...
populateUser(user, username, password, realname, email, siteId);
// ...and stick it into the database
return insertUser(user);
}
/**
* Looks up a user by username.
*
* @return the user with the specified user id or null if no user with that id exists.
*/
public User loadUser (String username)
throws PersistenceException
{
return loadUserWhere("where username = " + JDBCUtil.escape(username));
}
/**
* Looks up a user by userid.
*
* @return the user with the specified user id or null if no user with that id exists.
*/
public User loadUser (int userId)
throws PersistenceException
{
return loadUserWhere("where userId = " + userId);
}
/**
* Looks up a user by a session identifier.
*
* @return the user associated with the specified session or null of no session exists with the
* supplied identifier.
*/
public User loadUserBySession (String sessionKey)
throws PersistenceException
{
User user = load(_utable, "sessions", "where authcode = '" + sessionKey + "' " +
"AND sessions.userId = users.userId");
if (user != null) {
user.setDirtyMask(_utable.getFieldMask());
}
return user;
}
/**
* Looks up users by userid
*
* @return the users whom have a user id in the userIds array.
*/
public HashIntMap<User> loadUsersFromId (int[] userIds)
throws PersistenceException
{
HashIntMap<User> data = new HashIntMap<User>();
if (userIds.length > 0) {
String query = "where userId in (" + genIdString(userIds) + ")";
for (User user : loadAll(_utable, query)) {
user.setDirtyMask(_utable.getFieldMask());
data.put(user.userId, user);
}
}
return data;
}
/**
* Lookup a user by email address, something that is not efficient and should really only be
* done by site admins attempting to look up a user record.
*
* @return the user with the specified user id or null if no user with that id exists.
*/
public ArrayList<User> lookupUsersByEmail (String email)
throws PersistenceException
{
return loadAll(_utable, "where email = " + JDBCUtil.escape(email));
}
/**
* Looks up a list of users that match an arbitrary query. Care should be taken in constructing
* these queries as the user table is likely to be large and a query that does not make use of
* indices could be very slow.
*
* @return the users matching the specified query or an empty list if there are no matches.
*/
public ArrayList<User> lookupUsersWhere (final String where)
throws PersistenceException
{
ArrayList<User> users = loadAll(_utable, where);
for (User user : users) {
// configure the user record with its field mask
user.setDirtyMask(_utable.getFieldMask());
}
return users;
}
/**
* Updates a user that was previously fetched from the repository. Only fields that have been
* modified since it was loaded will be written to the database and those fields will
* subsequently be marked clean once again.
*
* @return true if the record was updated, false if the update was skipped because no fields in
* the user record were modified.
*/
public boolean updateUser (final User user)
throws PersistenceException
{
if (!user.getDirtyMask().isModified()) {
// nothing doing!
return false;
}
update(_utable, user, user.getDirtyMask());
return true;
}
/**
* 'Delete' the users account such that they can no longer access it, however we do not delete
* the record from the db. The name is changed such that the original name has XX=FOO if the
* name were FOO originally. If we have to lop off any of the name to get our prefix to fit we
* use a minus sign instead of a equals side. The password field is set to be the empty string
* so that no one can log in (since nothing hashes to the empty string. We also make sure
* their email address no longer works, so in case we don't ignore 'deleted' users when we do
* the sql to get emailaddresses for the mass mailings we still won't spam delete folk. We
* leave the emailaddress intact exect for the @ sign which gets turned to a #, so that we can
* see what their email was incase it was an accidently deletion and we have to verify through
* email.
*/
public void deleteUser (final User user)
throws PersistenceException
{
if (user.isDeleted()) {
return;
}
executeUpdate(new Operation<Object>() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
// create our modified fields mask
FieldMask mask = _utable.getFieldMask();
mask.setModified("username");
mask.setModified("password");
mask.setModified("email");
// set the password to unusable
user.password = "";
// 'disable' their email address
String newEmail = user.email.replace('@','#');
user.email = newEmail;
String oldName = user.username;
for (int ii = 0; ii < 100; ii++) {
try {
user.username = StringUtil.truncate(ii + "=" + oldName, 24);
_utable.update(conn, user, mask);
return null; // nothing to return
} catch (SQLException se) {
if (!liaison.isDuplicateRowException(se)) {
throw se;
}
}
}
// ok we failed to rename the user, lets bust an error
throw new PersistenceException("Failed to 'delete' the user");
}
});
}
/**
* Creates a new session for the specified user and returns the randomly generated session
* identifier for that session. If a session entry already exists for the specified user it
* will be reused.
*
* @param expireDays the number of days in which the session token should expire.
*/
public String registerSession (User user, int expireDays)
throws PersistenceException
{
// look for an existing session for this user
final String query = "select authcode from sessions where userId = " + user.userId;
String authcode = execute(new Operation<String>() {
public String invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
Statement stmt = conn.createStatement();
try {
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
return rs.getString(1);
}
return null;
} finally {
JDBCUtil.close(stmt);
}
}
});
// figure out when to expire the session
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, expireDays);
Date expires = new Date(cal.getTime().getTime());
// if we found one, update its expires time and reuse it
if (authcode != null) {
update("update sessions set expires = '" + expires + "' where " +
"authcode = '" + authcode + "'");
} else {
// otherwise create a new one and insert it into the table
authcode = UserUtil.genAuthCode(user);
update("insert into sessions (authcode, userId, expires) values('" + authcode + "', " +
user.userId + ", '" + expires + "')");
}
return authcode;
}
/**
* Validates that the supplied session key is still valid and if so, refreshes it for the
* specified number of days.
*
* @return true if the session was located and refreshed, false if it no longer exists.
*/
public boolean refreshSession (String sessionKey, int expireDays)
throws PersistenceException
{
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, expireDays);
Date expires = new Date(cal.getTime().getTime());
// attempt to update an existing session row, returning true if we found and updated it
return (update("update sessions set expires = '" + expires + "' " +
"where authcode = " + JDBCUtil.escape(sessionKey)) == 1);
}
/**
* Prunes any expired sessions from the sessions table.
*/
public void pruneSessions ()
throws PersistenceException
{
update("delete from sessions where expires <= CURRENT_DATE()");
}
/**
* Loads the usernames of the users identified by the supplied user ids and returns them in an
* array. If any users do not exist, their slot in the array will contain a null.
*/
public String[] loadUserNames (int[] userIds)
throws PersistenceException
{
return loadNames(userIds, "username");
}
/**
* Loads the real names of the users identified by the supplied user ids and returns them in an
* array. If any users do not exist, their slot in the array will contain a null.
*/
public String[] loadRealNames (int[] userIds)
throws PersistenceException
{
return loadNames(userIds, "realname");
}
/**
* Returns an array with the real names of every user in the system.
*/
public String[] loadAllRealNames ()
throws PersistenceException
{
final ArrayList<String> names = new ArrayList<String>();
// do the query
execute(new Operation<Object>() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
Statement stmt = conn.createStatement();
try {
String query = "select realname from users";
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
names.add(rs.getString(1));
}
// nothing to return
return null;
} finally {
JDBCUtil.close(stmt);
}
}
});
// finally construct our result
return names.toArray(new String[names.size()]);
}
/**
* Configures the supplied user record with the provided information in preparation for
* inserting the record into the database for the first time.
*/
protected void populateUser (
User user, Username name, Password pass, String realname, String email, int siteId)
{
user.username = name.getUsername();
user.setPassword(pass);
user.setRealName(realname);
user.setEmail(email);
user.created = new Date(System.currentTimeMillis());
user.setSiteId(siteId);
}
/**
* Inserts the supplied user record into the user database, assigning it a userid in the
* process, which is returned.
*/
protected int insertUser (final User user)
throws UserExistsException, PersistenceException
{
executeUpdate(new Operation<Object>() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
try {
_utable.insert(conn, user);
// update the userid now that it's known
user.userId = liaison.lastInsertedId(conn, _utable.getName(), "userId");
// nothing to return
return null;
} catch (SQLException sqe) {
if (liaison.isDuplicateRowException(sqe)) {
throw new UserExistsException("error.user_exists");
} else {
throw sqe;
}
}
}
});
return user.userId;
}
/**
* Loads up a user record that matches the specified where clause. Returns null if no record
* matches.
*/
protected User loadUserWhere (String where)
throws PersistenceException
{
User user = load(_utable, where);
if (user != null) {
user.setDirtyMask(_utable.getFieldMask());
}
return user;
}
protected String[] loadNames (int[] userIds, final String column)
throws PersistenceException
{
// if userids is zero length, we've got no work to do
if (userIds.length == 0) {
return new String[0];
}
// do the query
final String ids = genIdString(userIds);
final HashIntMap<String> map = new HashIntMap<String>();
execute(new Operation<Object>() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
Statement stmt = conn.createStatement();
try {
String query = "select userId, " + column + " from users " +
"where userId in (" + ids + ")";
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
int userId = rs.getInt(1);
String name = rs.getString(2);
map.put(userId, name);
}
// nothing to return
return null;
} finally {
JDBCUtil.close(stmt);
}
}
});
// finally construct our result
String[] result = new String[userIds.length];
for (int i = 0; i < userIds.length; i++) {
result[i] = map.get(userIds[i]);
}
return result;
}
/**
* Take the passed in int array and create the a string suitable for using in a SQL set query
* (I.e., "select foo, from bar where userId in (genIdString(userIds))"; )
*/
protected String genIdString (int[] userIds)
{
// build up the string we need for the query
StringBuilder ids = new StringBuilder();
for (int i = 0; i < userIds.length; i++) {
if (ids.length() > 0) {
ids.append(",");
}
ids.append(userIds[i]);
}
return ids.toString();
}
@Override
protected void createTables ()
{
// create our table object
_utable = new Table<User>(User.class, "users", "userId");
}
/** A wrapper that provides access to the userstable. */
protected Table<User> _utable;
}
@@ -0,0 +1,76 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.user;
import com.samskivert.util.Crypt;
import com.samskivert.util.StringUtil;
/**
* User related utility functions.
*/
public class UserUtil
{
/**
* Generates a new random session identifier for the supplied user.
*/
public static String genAuthCode (User user)
{
// concatenate a bunch of secret stuff together
StringBuilder buf = new StringBuilder();
buf.append(user.password);
buf.append(System.currentTimeMillis());
buf.append(Math.random());
// and MD5 hash it
return StringUtil.md5hex(buf.toString());
}
/**
* Encrypts the supplied username and password and returns the value
* that would be stored in the user record were the password to be
* updated via {@link User#setPassword}.
*/
public static String encryptPassword (String password)
{
return StringUtil.md5hex(password);
}
/**
* Encrypts passwords the way we used to.
*/
public static String legacyEncrypt (String username, String password,
boolean ignoreUserCase)
{
if (ignoreUserCase) {
username = username.toLowerCase();
}
return Crypt.crypt(StringUtil.truncate(username, 2), password);
}
public static void main (String[] args)
{
if (args.length < 2) {
System.err.println("Usage: UserUtil password");
System.exit(-1);
}
System.out.println("Encrypted password: " + encryptPassword(args[0]));
}
}
@@ -0,0 +1,83 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.user;
/**
* Allows us to require a valid username as a parameter without having to
* do the checking ourselves.
*/
public class Username
{
/** The minimum allowable length of a username. */
public static final int MINIMUM_USERNAME_LENGTH = 3;
/** The maximum allowable length of a username. */
public static final int MAXIMUM_USERNAME_LENGTH = 12;
/** The regular expression defining valid names. */
public static final String NAME_REGEX = "^[_A-Za-z0-9]*$";
/**
* Creates a username instance.
*/
public Username (String username)
throws InvalidUsernameException
{
validateName(username);
_username = username;
}
/** Returns the text of this username. */
public String getUsername ()
{
return _username;
}
@Override
public String toString ()
{
return _username;
}
/**
* Validates our username. The default implementation requires that usernames consist only of
* characters that match the {@link #NAME_REGEX} regular expression and be between {@link
* #MINIMUM_USERNAME_LENGTH} and {@link #MAXIMUM_USERNAME_LENGTH} characters.
*/
protected void validateName (String username)
throws InvalidUsernameException
{
// check length
if (username.length() < MINIMUM_USERNAME_LENGTH) {
throw new InvalidUsernameException("error.username_too_short");
}
if (username.length() > MAXIMUM_USERNAME_LENGTH) {
throw new InvalidUsernameException("error.username_too_long");
}
// check that it's only valid characters
if (!username.matches(NAME_REGEX)) {
throw new InvalidUsernameException("error.invalid_username");
}
}
protected String _username;
}
@@ -0,0 +1,56 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
$Id: package.html,v 1.1 2001/08/12 01:34:31 mdb Exp $
samskivert library - useful routines for java programs
Copyright (C) 2001 Michael Bayne
This library 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 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-->
</head>
<body bgcolor="white">
Provides services for managing a database of users in a web
application or suite of web applications.
<p> This is accomplished through the
{@link com.samskivert.servlet.user.UserManager} which can be used to
load user objects, create new users, update existing users and
generally all of the user-related things that you'd like to do. The
user manager manages cookies for persistent or session-only
authentication and provides support for requiring that a user be
authenticated in order to access a page.
<p> The user manager stores user information in an SQL database which
is accomplished by the
{@link com.samskivert.servlet.user.UserRepository} class. Presently,
it assumes that it is making use of a MySQL database, but the
dependencies are minimal. Eventually I'll modify the repository
abstraction so that the database-specific support code can be chosen
at runtime rather than compile time. Why I designed it the way it is
in the first place, I can't imagine. I'm a bad monkey.
<p> The user table (and associated
{@link com.samskivert.servlet.user.User} object) are inentionally
minimal. The expectation is that application specific data will be
stored and accessed via some other table that is keyed on the userid
provided by these services.
</body>
</html>
@@ -0,0 +1,90 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.util;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Utility methods for dealing with cookies.
*/
public class CookieUtil
{
/**
* Get the cookie of the specified name, or null if not found.
*/
public static Cookie getCookie (HttpServletRequest req, String name)
{
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (int ii=0, nn=cookies.length; ii < nn; ii++) {
if (cookies[ii].getName().equals(name)) {
return cookies[ii];
}
}
}
return null; // not found
}
/**
* Get the value of the cookie for the cookie of the specified name, or null if not found.
*/
public static String getCookieValue (HttpServletRequest req, String name)
{
Cookie c = getCookie(req, name);
return (c == null) ? null : c.getValue();
}
/**
* Clear the cookie with the specified name.
*/
public static void clearCookie (HttpServletResponse rsp, String name)
{
Cookie c = new Cookie(name, "x");
c.setPath("/");
c.setMaxAge(0);
rsp.addCookie(c);
}
/**
* Sets the domain of the specified cookie to the server name associated with the supplied
* request minus the hostname (ie. <code>www.samskivert.com</code> becomes
* <code>.samskivert.com</code>).
*/
public static void widenDomain (HttpServletRequest req, Cookie cookie)
{
String server = req.getServerName();
int didx = server.indexOf(".");
// if no period was found (e.g. localhost) don't set the domain
if (didx == -1) {
return;
}
// if two or more periods are found (e.g. www.domain.com) strip up to the first one
if (server.indexOf(".", didx+1) != -1) {
cookie.setDomain(server.substring(didx));
} else {
// ...otherwise prepend a "." because we're seeing something like "domain.com"
cookie.setDomain("." + server);
}
}
}
@@ -0,0 +1,35 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.util;
/**
* A data validation exception is thrown when a value supplied in a form
* element is not valid.
*
* @see ParameterUtil
*/
public class DataValidationException extends FriendlyException
{
public DataValidationException (String message)
{
super(message);
}
}
@@ -0,0 +1,163 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.util;
import java.io.*;
import java.util.*;
import com.samskivert.util.ConfigUtil;
import static com.samskivert.Log.log;
/**
* The exception map is used to map exceptions to error messages based on
* a static, server-wide configuration.
*
* <p>The configuration file is loaded via the classpath. The file should
* be named <code>exceptionmap.properties</code> and placed in the
* classpath of the JVM in which the servlet is executed. The file should
* contain colon-separated mappings from exception classes to friendly
* error messages. For example:
*
* <pre>
* # Exception mappings (lines beginning with # are ignored)
* com.samskivert.servlet.util.FriendlyException: An error occurred while \
* processing your request: {m}
*
* # lines ending with \ are continued on the next line
* java.sql.SQLException: The database is currently unavailable. Please \
* try your request again later.
*
* java.lang.Exception: An unexpected error occurred while processing \
* your request. Please try again later.
* </pre>
*
* The message associated with the exception will be substituted into the
* error string in place of <code>{m}</code>. The exceptions should be
* listed in order of most to least specific, as the first mapping for
* which the exception to report is an instance of the listed exception
* will be used.
*
* <p><em>Note:</em> These exception mappings will generally be used for
* all requests (perhaps some day only for requests associated with a
* particular application). Regardless, this error handling mechanism
* should not be used for request specific errors. For example, an SQL
* exception reporting a duplicate key should probably be caught and
* reported specifically by the appropriate populator (it can still
* leverage the pattern of inserting the error message into the context as
* <code>"error"</code>) rather than relying on the default SQL exception
* error message which is not likely to be meaningful for such a
* situation.
*/
public class ExceptionMap
{
/**
* Searches for the <code>exceptionmap.properties</code> file in the
* classpath and loads it. If the file could not be found, an error is
* reported and a default set of mappings is used.
*/
public static synchronized void init ()
{
// only initialize ourselves once
if (_keys != null) {
return;
} else {
_keys = new ArrayList<Class<?>>();
_values = new ArrayList<String>();
}
// first try loading the properties file without a leading slash
ClassLoader cld = ExceptionMap.class.getClassLoader();
InputStream config = ConfigUtil.getStream(PROPS_NAME, cld);
if (config == null) {
log.warning("Unable to load " + PROPS_NAME + " from CLASSPATH.");
} else {
// otherwise process ye old config file.
try {
// we'll do some serious jiggery pokery to leverage the parsing
// implementation provided by java.util.Properties. god bless
// method overloading
final ArrayList<String> classes = new ArrayList<String>();
Properties loader = new Properties() {
@Override public Object put (Object key, Object value) {
classes.add((String)key);
_values.add((String)value);
return key;
}
};
loader.load(config);
// now cruise through and resolve the exceptions named as
// keys and throw out any that don't appear to exist
for (int i = 0; i < classes.size(); i++) {
String exclass = classes.get(i);
try {
Class<?> cl = Class.forName(exclass);
// replace the string with the class object
_keys.add(cl);
} catch (Throwable t) {
log.warning("Unable to resolve exception class.", "class", exclass,
"error", t);
_values.remove(i);
i--; // back on up a notch
}
}
} catch (IOException ioe) {
log.warning("Error reading exception mapping file: " + ioe);
}
}
}
/**
* Looks up the supplied exception in the map and returns the most
* specific error message available for exceptions of that class.
*
* @param ex The exception to resolve into an error message.
*
* @return The error message to which this exception maps (properly
* populated with the message associated with this exception
* instance).
*/
public static String getMessage (Throwable ex)
{
String msg = DEFAULT_ERROR_MSG;
for (int i = 0; i < _keys.size(); i++) {
Class<?> cl = _keys.get(i);
if (cl.isInstance(ex)) {
msg = _values.get(i);
break;
}
}
return msg.replace(MESSAGE_MARKER, ex.getMessage());
}
protected static List<Class<?>> _keys;
protected static List<String> _values;
// initialize ourselves
static { init(); }
protected static final String PROPS_NAME = "exceptionmap.properties";
protected static final String DEFAULT_ERROR_MSG = "Error: {m}";
protected static final String MESSAGE_MARKER = "{m}";
}
@@ -0,0 +1,34 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.util;
/**
* The friendly exception provides a mechanism by which a servlet or
* underlying code can abort its processing and report a human readable
* error to the servlet framework.
*/
public class FriendlyException extends Exception
{
public FriendlyException (String message)
{
super(message);
}
}
@@ -0,0 +1,243 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.util;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.samskivert.util.StringUtil;
/**
* HTML related utility functions.
*/
public class HTMLUtil
{
/**
* Converts instances of <code><, >, & and "</code> into their
* entified equivalents: <code>&lt;, &gt;, &amp; and &quot;</code>.
* These characters are mentioned in the HTML spec as being common
* candidates for entification.
*
* @return the entified string.
*/
public static String entify (String text)
{
// this could perhaps be done more efficiently, but this function
// is not likely to be called on large quantities of text
// (first we turn the entified versions normal so that if text is
// repeatedly run through this it doesn't keep changing successive
// &'s into "&amp;".
text = text.replace("&quot;", "\"");
text = text.replace("&gt;", ">");
text = text.replace("&lt;", "<");
text = text.replace("&amp;", "&");
text = text.replace("&", "&amp;");
text = text.replace("<", "&lt;");
text = text.replace(">", "&gt;");
text = text.replace("\"", "&quot;");
return text;
}
/**
* Inserts a &lt;p&gt; tag between every two consecutive newlines.
*/
public static String makeParagraphs (String text)
{
if (text == null) {
return text;
}
// handle both line ending formats
text = text.replace("\n\n", "\n<p>\n");
text = text.replace("\r\n\r\n", "\r\n<p>\r\n");
return text;
}
/**
* Inserts a &lt;br&gt; tag before every newline.
*/
public static String makeLinear (String text)
{
if (text == null) {
return text;
}
// handle both line ending formats
text = text.replace("\n", "<br>\n");
return text;
}
/**
* Does some simple HTML markup, matching bare image URLs and wrapping
* them in image tags, matching other URLs and wrapping them in href
* tags, and wrapping * prefixed lists into ul-style HTML lists.
*/
public static String simpleFormat (String text)
{
// first replace the image and other URLs
Matcher m = URL_PAT.matcher(text);
StringBuffer tbuf = new StringBuffer();
while (m.find()) {
String match = m.group();
String lmatch = match.toLowerCase();
if (lmatch.endsWith(".png") ||
lmatch.endsWith(".jpg") ||
lmatch.endsWith(".gif")) {
match = "<img src=\"" + match + "\">";
} else {
match = "<a href=\"" + match + "\">" + match + "</a>";
}
m.appendReplacement(tbuf, match);
}
m.appendTail(tbuf);
// then do our paragraph and list processing
String[] lines = StringUtil.split(tbuf.toString(), "\n");
StringBuilder lbuf = new StringBuilder();
boolean inpara = false, inlist = false;
for (String line2 : lines) {
String line = line2;
if (StringUtil.isBlank(line2)) {
if (inlist) {
lbuf.append("</ul>");
inlist = false;
}
if (inpara) {
lbuf.append("</p>\n");
inpara = false;
}
continue;
}
if (!inpara) {
inpara = true;
lbuf.append("<p> ");
}
if (line.startsWith("*")) {
if (inlist) {
lbuf.append("<li>");
} else {
lbuf.append("<ul><li>");
inlist = true;
}
line = line.substring(1);
}
lbuf.append(line).append("\n");
}
if (inlist) {
lbuf.append("</ul>");
}
if (inpara) {
lbuf.append("</p>\n");
}
return lbuf.toString().trim();
}
/**
* Restrict all HTML from the specified String.
*/
public static String restrictHTML (String src)
{
return restrictHTML(src, new String[0]);
}
/**
* Restrict HTML except for the specified tags.
*
* @param allowFormatting enables &lt;i&gt;, &lt;b&gt;, &lt;u&gt;,
* &lt;font&gt;, &lt;br&gt;, &lt;p&gt;, and
* &lt;hr&gt;.
* @param allowImages enabled &lt;img ...&gt;.
* @param allowLinks enabled &lt;a href ...&gt;.
*/
public static String restrictHTML (String src, boolean allowFormatting,
boolean allowImages, boolean allowLinks)
{
// TODO: these regexes should probably be checked to make
// sure that javascript can't live inside a link
ArrayList<String> allow = new ArrayList<String>();
if (allowFormatting) {
allow.add("<b>"); allow.add("</b>");
allow.add("<i>"); allow.add("</i>");
allow.add("<u>"); allow.add("</u>");
allow.add("<font [^\"<>!-]*(\"[^\"<>!-]*\"[^\"<>!-]*)*>");
allow.add("</font>");
allow.add("<br>"); allow.add("</br>"); allow.add("<br/>");
allow.add("<p>"); allow.add("</p>");
allow.add("<hr>"); allow.add("</hr>"); allow.add("<hr/>");
}
if (allowImages) {
// Until I find a way to disallow "---", no - can be in a url
allow.add("<img [^\"<>!-]*(\"[^\"<>!-]*\"[^\"<>!-]*)*>");
allow.add("</img>");
}
if (allowLinks) {
allow.add("<a href=[^\"<>!-]*(\"[^\"<>!-]*\"[^\"<>!-]*)*>");
allow.add("</a>");
}
return restrictHTML(src, allow.toArray(new String[allow.size()]));
}
/**
* Restrict HTML from the specified string except for the specified
* regular expressions.
*/
public static String restrictHTML (String src, String[] regexes)
{
if (StringUtil.isBlank(src)) {
return src;
}
ArrayList<String> list = new ArrayList<String>();
list.add(src);
for (String regexe : regexes) {
Pattern p = Pattern.compile(regexe, Pattern.CASE_INSENSITIVE);
for (int jj=0; jj < list.size(); jj += 2) {
String piece = list.get(jj);
Matcher m = p.matcher(piece);
if (m.find()) {
list.set(jj, piece.substring(0, m.start()));
list.add(jj + 1, piece.substring(m.start(), m.end()));
list.add(jj + 2, piece.substring(m.end()));
}
}
}
// now, the even elements of list contain untrusted text, the
// odd elements contain stuff that matched a regex
StringBuilder buf = new StringBuilder();
for (int jj=0, nn = list.size(); jj < nn; jj++) {
String s = list.get(jj);
if (jj % 2 == 0) {
s = s.replace("<", "&lt;");
s = s.replace(">", "&gt;");
}
buf.append(s);
}
return buf.toString();
}
protected static final Pattern URL_PAT = Pattern.compile("^http://\\S+", Pattern.MULTILINE);
}
@@ -0,0 +1,416 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.util;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import com.samskivert.util.ArrayIntSet;
import com.samskivert.util.IntSet;
import com.samskivert.util.StringUtil;
/**
* Utility functions for fetching and manipulating request parameters (form fields).
*/
public class ParameterUtil
{
/** A default date that can be placed in fields to communicate the appropriate format. Is
* treated the same as the empty string by {@link #getDateParameter}. */
public static final String DATE_TEMPLATE = "YYYY-MM-DD";
/**
* An interface for validating form parameters.
*/
public static interface ParameterValidator
{
public void validateParameter (String name, String value)
throws DataValidationException;
}
/**
* Returns the names of the parameters of the supplied request in a civilized format.
*/
public static Iterable<String> getParameterNames (HttpServletRequest req)
{
List<String> params = new ArrayList<String>();
Enumeration<?> iter = req.getParameterNames();
while (iter.hasMoreElements()) {
params.add((String)iter.nextElement());
}
return params;
}
/**
* Fetches the supplied parameter from the request. If the parameter does not exist, either
* null or the empty string will be returned depending on the value of the
* <code>returnNull</code> parameter.
*/
public static String getParameter (HttpServletRequest req, String name, boolean returnNull)
{
String value = req.getParameter(name);
if (value == null) {
return returnNull ? null : "";
} else {
return value.trim();
}
}
/**
* Fetches the supplied parameter from the request and converts it to a float. If the parameter
* does not exist or is not a well-formed float, a data validation exception is thrown with the
* supplied message.
*/
public static float requireFloatParameter (
HttpServletRequest req, String name, String invalidDataMessage)
throws DataValidationException
{
return parseFloatParameter(getParameter(req, name, false), invalidDataMessage);
}
/**
* Fetches the supplied parameter from the request and converts it to an integer. If the
* parameter does not exist or is not a well-formed integer, a data validation exception is
* thrown with the supplied message.
*/
public static int requireIntParameter (
HttpServletRequest req, String name, String invalidDataMessage)
throws DataValidationException
{
return parseIntParameter(getParameter(req, name, false), invalidDataMessage);
}
/**
* Fetches the supplied parameter from the request and converts it to an integer. If the
* parameter does not exist or is not a well-formed integer, a data validation exception is
* thrown with the supplied message.
*/
public static int requireIntParameter (HttpServletRequest req, String name,
String invalidDataMessage, ParameterValidator validator)
throws DataValidationException
{
String value = getParameter(req, name, false);
validator.validateParameter(name, value);
return parseIntParameter(value, invalidDataMessage);
}
/**
* Fetches the supplied parameter from the request and converts it to an integer. If the
* parameter does not exist or is not a well-formed integer, a data validation exception is
* thrown with the supplied message.
*/
public static int requireIntParameter (HttpServletRequest req, String name, int low, int high,
String invalidDataMessage)
throws DataValidationException
{
return requireIntParameter(
req, name, invalidDataMessage, new IntRangeValidator(low, high, invalidDataMessage));
}
/**
* Fetches all the values from the request with the specified name and converts them to an
* IntSet. If the parameter does not exist or is not a well-formed integer, a data validation
* exception is thrown with the supplied message.
*/
public static IntSet getIntParameters (
HttpServletRequest req, String name, String invalidDataMessage)
throws DataValidationException
{
IntSet ints = new ArrayIntSet();
String[] values = req.getParameterValues(name);
if (values != null) {
for (int ii = 0; ii < values.length; ii++) {
if (!StringUtil.isBlank(values[ii])) {
ints.add(parseIntParameter(values[ii], invalidDataMessage));
}
}
}
return ints;
}
/**
* Fetches all the values from the request with the specified name and converts them to a Set.
*/
public static Set<String> getParameters (HttpServletRequest req, String name)
{
Set<String> set = new HashSet<String>();
String[] values = req.getParameterValues(name);
if (values != null) {
for (int ii = 0; ii < values.length; ii++) {
if (!StringUtil.isBlank(values[ii])) {
set.add(values[ii]);
}
}
}
return set;
}
/**
* Fetches the supplied parameter from the request. If the parameter does not exist,
* <code>defval</code> is returned.
*/
public static String getParameter (HttpServletRequest req, String name, String defval)
{
String value = req.getParameter(name);
return StringUtil.isBlank(value) ? defval : value.trim();
}
/**
* Fetches the supplied parameter from the request and converts it to an integer. If the
* parameter does not exist, <code>defval</code> is returned. If the parameter is not a
* well-formed integer, a data validation exception is thrown with the supplied message.
*/
public static int getIntParameter (
HttpServletRequest req, String name, int defval, String invalidDataMessage)
throws DataValidationException
{
String value = getParameter(req, name, false);
if (StringUtil.isBlank(value)) {
return defval;
}
return parseIntParameter(value, invalidDataMessage);
}
/**
* Fetches the supplied parameter from the request and converts it to a long. If the parameter
* does not exist, <code>defval</code> is returned. If the parameter is not a well-formed
* integer, a data validation exception is thrown with the supplied message.
*/
public static long getLongParameter (
HttpServletRequest req, String name, long defval, String invalidDataMessage)
throws DataValidationException
{
String value = getParameter(req, name, false);
if (StringUtil.isBlank(value)) {
return defval;
}
return parseLongParameter(value, invalidDataMessage);
}
/**
* Fetches the supplied parameter from the request. If the parameter does not exist, a data
* validation exception is thrown with the supplied message.
*/
public static String requireParameter (
HttpServletRequest req, String name, String missingDataMessage)
throws DataValidationException
{
String value = getParameter(req, name, true);
if (StringUtil.isBlank(value)) {
throw new DataValidationException(missingDataMessage);
}
return value;
}
/**
* Fetches the supplied parameter from the request and ensures that it is no longer than
* maxLength.
*
* Note that use of this method could be dangerous. If the specified HttpServletRequest is used
* to pre-fill in values on a form, it will not know to use the truncated version. A user may
* enter a version that is too long, your code will see a truncated version, but then the user
* will see on the page again the full-length reproduction of what they typed in. Be careful.
*/
public static String requireParameter (
HttpServletRequest req, String name, String missingDataMessage, int maxLength)
throws DataValidationException
{
return StringUtil.truncate(
requireParameter(req, name, missingDataMessage), maxLength);
}
/**
* Fetches the supplied parameter from the request and converts it to a date. The value of the
* parameter should be a date formatted like so: 2001-12-25. If the parameter does not exist or
* is not a well-formed date, a data validation exception is thrown with the supplied message.
*/
public static Date requireDateParameter (
HttpServletRequest req, String name, String invalidDataMessage)
throws DataValidationException
{
return parseDateParameter(getParameter(req, name, false), invalidDataMessage);
}
/**
* Similar to {@link #requireDateParameter} but returns a SQL date.
*/
public static java.sql.Date requireSQLDateParameter (
HttpServletRequest req, String name, String invalidDataMessage)
throws DataValidationException
{
return new java.sql.Date(requireDateParameter(req, name, invalidDataMessage).getTime());
}
/**
* Fetches the supplied parameter from the request and converts it to a date. The value of the
* parameter should be a date formatted like so: 2001-12-25. If the parameter does not exist,
* null is returned. If the parameter is not a well-formed date, a data validation exception is
* thrown with the supplied message.
*/
public static Date getDateParameter (
HttpServletRequest req, String name, String invalidDataMessage)
throws DataValidationException
{
String value = getParameter(req, name, false);
if (StringUtil.isBlank(value) ||
DATE_TEMPLATE.equalsIgnoreCase(value)) {
return null;
}
return parseDateParameter(value, invalidDataMessage);
}
/**
* Similar to {@link #getDateParameter} but returns a SQL date.
*/
public static java.sql.Date getSQLDateParameter (
HttpServletRequest req, String name, String invalidDataMessage)
throws DataValidationException
{
Date when = getDateParameter(req, name, invalidDataMessage);
return (when == null) ? null : new java.sql.Date(when.getTime());
}
/**
* Returns true if the specified parameter is set in the request.
*
* @return true if the specified parameter is set in the request context, false otherwise.
*/
public static boolean isSet (HttpServletRequest req, String name)
{
return isSet(req, name, false);
}
/**
* Returns true if the specified parameter is set to a non-blank value in the request, false if
* the value is blank, or <code>defaultValue</code> if the parameter is unspecified.
*
* @return true if the specified parameter is set to a non-blank value in the request, false if
* the value is blank, or <code>defaultValue</code> if the parameter is unspecified.
*/
public static boolean isSet (HttpServletRequest req, String name, boolean defaultValue)
{
String value = getParameter(req, name, true);
return (value == null) ? defaultValue : !StringUtil.isBlank(req.getParameter(name));
}
/**
* Returns true if the specified parameter is equal to the supplied value. If the parameter is
* not set in the request, false is returned.
*
* @param name The parameter whose value should be compared with the supplied value.
* @param value The value to which the parameter may be equal. This should not be null.
*
* @return true if the specified parameter is equal to the supplied parameter, false otherwise.
*/
public static boolean parameterEquals (HttpServletRequest req, String name, String value)
{
return value.equals(getParameter(req, name, false));
}
/**
* Internal method to parse integer values.
*/
protected static int parseIntParameter (String value, String invalidDataMessage)
throws DataValidationException
{
try {
return Integer.parseInt(value);
} catch (NumberFormatException nfe) {
throw new DataValidationException(invalidDataMessage);
}
}
/**
* Internal method to parse long values.
*/
protected static long parseLongParameter (String value, String invalidDataMessage)
throws DataValidationException
{
try {
return Long.parseLong(value);
} catch (NumberFormatException nfe) {
throw new DataValidationException(invalidDataMessage);
}
}
/**
* Internal method to parse a float value.
*/
protected static float parseFloatParameter (String value, String invalidDataMessage)
throws DataValidationException
{
try {
return Float.parseFloat(value);
} catch (NumberFormatException nfe) {
throw new DataValidationException(invalidDataMessage);
}
}
/**
* Internal method to parse a date.
*/
protected static Date parseDateParameter (String value, String invalidDataMessage)
throws DataValidationException
{
try {
synchronized (_dparser) {
return _dparser.parse(value);
}
} catch (ParseException pe) {
throw new DataValidationException(invalidDataMessage);
}
}
/** Makes sure integers are within a range. */
protected static class IntRangeValidator implements ParameterValidator
{
public IntRangeValidator (int low, int high, String outOfRangeError) {
_low = low;
_high = high;
_err = outOfRangeError;
}
public void validateParameter (String name, String value) throws DataValidationException {
try {
int ivalue = Integer.parseInt(value);
if ((ivalue >= _low) && (ivalue <= _high)) {
return;
}
} catch (Exception e) {
// fall through
}
throw new DataValidationException(_err);
}
protected int _low, _high;
protected String _err;
}
/** We use this to parse dates in requireDateParameter(). */
protected static final SimpleDateFormat _dparser = new SimpleDateFormat("yyyy-MM-dd");
}
@@ -0,0 +1,136 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.util;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import com.samskivert.util.StringUtil;
/**
* A repository of utility functions related to HTTP servlet stuff.
*/
public class RequestUtils
{
/**
* Reconstructs the current location (URL) from the request and
* servlet configuration (which is the only way to know which server
* we're on because that's not provided in the request) and returns it
* URL encoded so that it can be substituted into another URL.
*
* @return The URL encoded URL that represents our current location.
*/
public static String getLocationEncoded (HttpServletRequest req)
{
return StringUtil.encode(getLocation(req));
}
/**
* Reconstructs the current location (URL) from the request and
* servlet configuration (which is the only way to know which server
* we're on because that's not provided in the request) and returns
* it.
*
* @return The URL that represents our current location.
*/
public static String getLocation (HttpServletRequest req)
{
StringBuffer rurl = req.getRequestURL();
String qs = req.getQueryString();
if (qs != null) {
rurl.append("?").append(qs);
}
return rurl.toString();
}
/**
* Recreates the URL used to make the supplied request, replacing the
* server part of the URL with the supplied server name.
*/
public static String rehostLocation (
HttpServletRequest req, String servername)
{
StringBuffer buf = req.getRequestURL();
String csname = req.getServerName();
int csidx = buf.indexOf(csname);
if (csidx != -1) {
buf.delete(csidx, csidx + csname.length());
buf.insert(csidx, servername);
}
String query = req.getQueryString();
if (!StringUtil.isBlank(query)) {
buf.append("?").append(query);
}
return buf.toString();
}
/**
* Prepends the server, port and servlet context path to the supplied
* path, resulting in a fully-formed URL for requesting a servlet.
*/
public static String getServletURL (HttpServletRequest req, String path)
{
StringBuffer buf = req.getRequestURL();
String sname = req.getServletPath();
buf.delete(buf.length() - sname.length(), buf.length());
if (!path.startsWith("/")) {
buf.append("/");
}
buf.append(path);
return buf.toString();
}
/**
* Reconstructs the request URL including query parameters. <em>Note:</em>
* the output of this method is purely for logging purposes only, thus POST
* parameters are shown as if they were GET parameters and parameters are
* <em>not</em> URL encoded.
*/
public static String reconstructURL (HttpServletRequest req)
{
StringBuffer buf = req.getRequestURL();
@SuppressWarnings("unchecked") Map<String, String[]> map = req.getParameterMap();
if (map.size() > 0) {
buf.append("?");
for (Map.Entry<String, String[]> entry : map.entrySet()) {
if (buf.charAt(buf.length()-1) != '?') {
buf.append("&");
}
buf.append(entry.getKey()).append("=");
String[] values = entry.getValue();
if (values.length == 1) {
buf.append(values[0]);
} else {
buf.append("(");
for (int ii = 0; ii < values.length; ii++) {
if (ii > 0) {
buf.append(", ");
}
buf.append(values[ii]);
}
buf.append(")");
}
}
}
return buf.toString();
}
}
@@ -0,0 +1,65 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.servlet.util;
/**
* Extends the basic ServiceWaiter to be useful for servlets.
*/
public class ServiceWaiter<T> extends com.samskivert.util.ServiceWaiter<T>
{
/** Timeout to specify when you don't want a timeout. Use at your own
* risk. */
public static final int NO_TIMEOUT = -1;
/**
* Construct a ServiceWaiter with the default (30 second) timeout.
*/
public ServiceWaiter ()
{
super();
}
/**
* Construct a ServiceWaiter with the specified timeout.
*
* @param timeout the timeout, in seconds.
*/
public ServiceWaiter (int timeout)
{
super(timeout);
}
/**
* Blocks waiting for the response.
*
* @return true if a success response was posted, false if a failure
* repsonse was posted.
*/
public boolean awaitFriendlyResponse (String friendlyText)
throws FriendlyException
{
try {
return waitForResponse();
} catch (TimeoutException te) {
throw new FriendlyException(friendlyText);
}
}
}
@@ -0,0 +1,32 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
$Id: package.html,v 1.1 2001/08/12 01:34:31 mdb Exp $
samskivert library - useful routines for java programs
Copyright (C) 2001 Michael Bayne
This library 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 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-->
</head>
<body bgcolor="white">
Provides servlet-related utilities.
</body>
</html>
@@ -0,0 +1,64 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.awt.EventQueue;
import com.samskivert.util.ResultListener;
/**
* Dispatches a {@link ResultListener}'s callbacks on the AWT thread
* regardless of what thread on which they were originally dispatched.
*/
public class AWTResultListener<T> implements ResultListener<T>
{
/**
* Creates an AWT result listener that will dispatch results to the
* supplied target.
*/
public AWTResultListener (ResultListener<T> target)
{
_target = target;
}
// documentation inherited from interface
public void requestCompleted (final T result)
{
EventQueue.invokeLater(new Runnable() {
public void run () {
_target.requestCompleted(result);
}
});
}
// documentation inherited from interface
public void requestFailed (final Exception cause)
{
EventQueue.invokeLater(new Runnable() {
public void run () {
_target.requestFailed(cause);
}
});
}
/** The result listener for which we are proxying. */
protected ResultListener<T> _target;
}
@@ -0,0 +1,177 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager2;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.HashMap;
import static com.samskivert.Log.log;
/**
* Used to lay out components at absolute coordinates. This layout manager
* will not gracefully deal with not having room to layout the components
* at the coordinates specified, so be sure only to use it in very
* controlled circumstances.
*
* <p> Components should be added with a {@link Point} or {@link
* Rectangle} object as their constraints. If their constraints are a
* {@link Point} they will be laid out at those coordinates with their
* preferred size. If it is a {@link Rectangle} they will be laid out in
* those exact bounds.
*/
public class AbsoluteLayout
implements LayoutManager2
{
// documentation inherited from interface
public void addLayoutComponent (String name, Component comp)
{
throw new RuntimeException("You must use " +
"addLayoutComponent(Component, Object)");
}
// documentation inherited from interface
public void addLayoutComponent (Component comp, Object constraints)
{
if (!(constraints instanceof Point ||
constraints instanceof Rectangle)) {
throw new IllegalArgumentException("Constraints must be " +
"Point or Rectangle");
}
_constraints.put(comp, constraints);
}
// documentation inherited from interface
public void removeLayoutComponent (Component comp)
{
_constraints.remove(comp);
}
// documentation inherited from interface
public Dimension preferredLayoutSize (Container parent)
{
Rectangle rect = new Rectangle();
Rectangle temp = new Rectangle();
int pcount = parent.getComponentCount();
for (int ii = 0; ii < pcount; ii++) {
Component comp = parent.getComponent(ii);
if (!comp.isVisible()) {
continue;
}
Object constr = _constraints.get(comp);
if (constr == null) {
log.warning("No constraints for child!?", "cont", parent, "comp", comp);
continue;
}
if (constr instanceof Rectangle) {
rect.add((Rectangle)constr);
} else {
Point p = (Point)constr;
Dimension d = comp.getPreferredSize();
temp.setBounds(p.x, p.y, d.width, d.height);
rect.add(temp);
}
}
Dimension dims = new Dimension(rect.width, rect.height);
// account for the insets
Insets insets = parent.getInsets();
dims.width += insets.left + insets.right;
dims.height += insets.top + insets.bottom;
return dims;
}
// documentation inherited from interface
public Dimension minimumLayoutSize (Container parent)
{
// we don't do no fancy business
return preferredLayoutSize(parent);
}
// documentation inherited from interface
public Dimension maximumLayoutSize (Container parent)
{
// we don't do no fancy business
return preferredLayoutSize(parent);
}
// documentation inherited from interface
public void layoutContainer (Container parent)
{
Insets insets = parent.getInsets();
int pcount = parent.getComponentCount();
for (int ii = 0; ii < pcount; ii++) {
Component comp = parent.getComponent(ii);
if (!comp.isVisible()) {
continue;
}
Object constr = _constraints.get(comp);
if (constr == null) {
log.warning("No constraints for child!?", "cont", parent, "comp", comp);
continue;
}
if (constr instanceof Rectangle) {
Rectangle r = (Rectangle)constr;
comp.setBounds(insets.left + r.x, insets.top + r.y,
r.width, r.height);
} else {
Point p = (Point)constr;
Dimension d = comp.getPreferredSize();
comp.setBounds(insets.left + p.x, insets.top + p.y,
d.width, d.height);
}
}
}
// documentation inherited from interface
public void invalidateLayout (Container target)
{
// nothing to do here
}
// documentation inherited from interface
public float getLayoutAlignmentX (Container target)
{
return 0;
}
// documentation inherited from interface
public float getLayoutAlignmentY (Container target)
{
return 0;
}
protected HashMap<Component,Object> _constraints =
new HashMap<Component,Object>();
}
@@ -0,0 +1,109 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.ListModel;
/**
* Displays a list of components in sections (with section headers) that
* can be collapsed. Each section of a list uses a {@link JList} instance
* to render the section elements.
*/
public class CollapsibleList extends JPanel
{
/**
* Constructs an empty collapsible list.
*/
public CollapsibleList ()
{
// set up our layout manager
VGroupLayout gl = new VGroupLayout(VGroupLayout.NONE);
gl.setOffAxisPolicy(VGroupLayout.STRETCH);
gl.setJustification(VGroupLayout.TOP);
gl.setOffAxisJustification(VGroupLayout.LEFT);
setLayout(gl);
}
/**
* Constructs a collapsible list with the supplied section labels and
* models.
*/
public CollapsibleList (String[] sections, ListModel[] models)
{
this(); // set up our layout manager
for (int i = 0; i < sections.length; i++) {
addSection(sections[i], models[i]);
}
}
/**
* Returns the number of sections.
*/
public int getSectionCount ()
{
return getComponentCount()/2;
}
/**
* Adds a section to this collapsible list.
*
* @param label the title of the section.
* @param model the list model to use for the new section.
*
* @return the index of the newly added section.
*/
public int addSection (String label, ListModel model)
{
add(new JLabel(label));
add(new JList(model));
return getSectionCount()-1;
}
/**
* Returns the label object associated with the title of the specified
* section.
*/
public JLabel getSectionLabel (int index)
{
return (JLabel)getComponent(index*2);
}
/**
* Returns the list object associated with the specified section.
*/
public JList getSectionList (int index)
{
return (JList)getComponent(index*2+1);
}
/**
* Toggles the collapsed state of the specified section.
*/
public void toggleCollapsed (int index)
{
JList list = getSectionList(index);
list.setVisible(!list.isVisible());
}
}
@@ -0,0 +1,208 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.awt.EventQueue;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.swing.AbstractButton;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import com.samskivert.swing.util.SwingUtil;
/**
* A panel that contains a button which will collapse the rest of the content.
*/
public class CollapsiblePanel extends JPanel
implements ActionListener
{
/**
* Construct a collapsible panel with the specified button as the
* trigger. The text of the button will be used as the triggertext.
*/
public CollapsiblePanel (JButton trigger)
{
setTrigger(trigger, null, null);
setTriggerContainer(trigger);
}
/**
* Construct a collapsible panel with the specified button text.
*/
public CollapsiblePanel (String triggertext)
{
this(new JButton(triggertext));
}
/**
* Create a collapsible panel to which the trigger button will be
* added later.
*/
public CollapsiblePanel (LayoutManager layout)
{
setLayout(layout);
}
/**
* Create a collapsible panel to which the trigger button will be
* added later.
*/
public CollapsiblePanel ()
{
VGroupLayout gl = new VGroupLayout(VGroupLayout.NONE);
gl.setOffAxisPolicy(VGroupLayout.STRETCH);
gl.setGap(0);
gl.setJustification(VGroupLayout.TOP);
gl.setOffAxisJustification(VGroupLayout.LEFT);
setLayout(gl);
}
/**
* Set a component which contains the trigger button. The simple case
* is to just set the trigger button as this component.
*/
public void setTriggerContainer (JComponent comp)
{
setTriggerContainer(comp, new JPanel());
}
/**
* Set a component which contains the trigger button.
*/
public void setTriggerContainer (JComponent comp, JPanel content)
{
setTriggerContainer(comp, content, true);
}
/**
* Set a component which contains the trigger button.
*/
public void setTriggerContainer (JComponent comp, JPanel content, boolean collapsed)
{
// these are our only two components.
add(comp);
add(_content = content);
// When the content is shown, make sure it's scrolled visible
_content.addComponentListener(new ComponentAdapter() {
@Override public void componentShown (ComponentEvent event)
{
// we can't do it just yet, the content doesn't know its size
EventQueue.invokeLater(new Runnable() {
public void run () {
// The content is offset a bit from the trigger
// but we want the trigger to show up, so we add
// in point 0,0
Rectangle r = _content.getBounds();
r.add(0, 0);
scrollRectToVisible(r);
}
});
}
});
// and start us out not showing
setCollapsed(collapsed);
}
/**
* Set the trigger button.
*/
public void setTrigger (AbstractButton trigger,
Icon collapsed, Icon uncollapsed)
{
_trigger = trigger;
_trigger.setHorizontalAlignment(SwingConstants.LEFT);
_trigger.setHorizontalTextPosition(SwingConstants.RIGHT);
_downIcon = collapsed;
_upIcon = uncollapsed;
_trigger.addActionListener(this);
}
/**
* Set the gap between the trigger button and the rest of the content.
* Can be negative for an overlapping effect.
*/
public void setGap (int gap)
{
((VGroupLayout) getLayout()).setGap(gap);
invalidate();
}
/**
* Get the content panel for filling in with sweet content goodness.
*/
public JPanel getContent ()
{
return _content;
}
// documentation from interface ActionListener
public void actionPerformed (ActionEvent e)
{
if (e.getSource() == _trigger) {
setCollapsed(!isCollapsed());
}
}
/**
* Is the panel collapsed?
*/
public boolean isCollapsed ()
{
return !_content.isVisible();
}
/**
* Set the collapsion state.
*/
public void setCollapsed (boolean collapse)
{
if (collapse) {
_content.setVisible(false);
_trigger.setIcon(_downIcon);
} else {
_content.setVisible(true);
_trigger.setIcon(_upIcon);
}
SwingUtil.refresh(this);
}
/** The button that triggers collapsion. */
protected AbstractButton _trigger;
/** The icons for collapsed and uncollapsed. */
protected Icon _upIcon, _downIcon;
/** The who in the what now? */
protected JPanel _content;
}
@@ -0,0 +1,408 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.border.Border;
import javax.swing.BorderFactory;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import com.samskivert.swing.util.SwingUtil;
/**
* Used to display a horizontal or vertical array of buttons, out of which
* only one is selectable at a time (which will be represented by
* rendering it with an indented border, whereas the other buttons will
* render with an extruded border.
*/
public class ComboButtonBox extends JPanel
implements SwingConstants, ListDataListener, MouseListener
{
/**
* Constructs a button box with the specified orientation (either
* {@link #HORIZONTAL} or {@link #VERTICAL}.
*/
public ComboButtonBox (int orientation)
{
this(orientation, new DefaultComboBoxModel());
}
/**
* Constructs a button box with the specified orientation (either
* {@link #HORIZONTAL} or {@link #VERTICAL}. The supplied model will
* be used to populate the buttons (see {@link #setModel} for more
* details).
*/
public ComboButtonBox (int orientation, ComboBoxModel model)
{
// set up our layout
setOrientation(orientation);
// set up our contents
setModel(model);
}
/**
* Sets the orientation of the box (either {@link #HORIZONTAL} or
* {@link #VERTICAL}.
*/
public void setOrientation (int orientation)
{
GroupLayout gl = (orientation == HORIZONTAL) ?
(GroupLayout)new HGroupLayout() : new VGroupLayout();
gl.setPolicy(GroupLayout.EQUALIZE);
setLayout(gl);
}
/**
* Provides the button box with a data model which it will display. If
* the model contains {@link Image} objects, they will be used to make
* icons for the buttons. Otherwise the button text will contain the
* string representation of the elements in the model.
*/
public void setModel (ComboBoxModel model)
{
// if we had a previous model, unregister ourselves from it
if (_model != null) {
_model.removeListDataListener(this);
}
// subscribe to our new model
_model = model;
_model.addListDataListener(this);
// rebuild the list
removeAll();
addButtons(0, _model.getSize());
}
@Override
public void setEnabled (boolean enabled)
{
super.setEnabled(enabled);
int ccount = getComponentCount();
for (int i = 0; i < ccount; i++) {
getComponent(i).setEnabled(enabled);
}
}
/**
* Returns the model in use by the button box.
*/
public ComboBoxModel getModel ()
{
return _model;
}
/**
* Sets the index of the selected component. A value of -1 will clear
* the selection.
*/
public void setSelectedIndex (int selidx)
{
// update the display
updateSelection(selidx);
// let the model know what's up
Object item = (selidx == -1) ? null : _model.getElementAt(selidx);
_model.setSelectedItem(item);
}
/**
* Returns the index of the selected item.
*/
public int getSelectedIndex ()
{
return _selectedIndex;
}
/**
* Specifies the command that will be used when generating action
* events (which is done when the selection changes).
*/
public void setActionCommand (String actionCommand)
{
_actionCommand = actionCommand;
}
/**
* Adds a listener to our list of entities to be notified when the
* selection changes.
*/
public void addActionListener (ActionListener l)
{
listenerList.add(ActionListener.class, l);
}
/**
* Removes a listener from the list.
*/
public void removeActionListener (ActionListener l)
{
listenerList.remove(ActionListener.class, l);
}
/**
* Notifies our listeners when the selection changed.
*/
protected void fireActionPerformed ()
{
// guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// process the listeners last to first, notifying those that are
// interested in this event
for (int i = listeners.length-2; i >= 0; i -= 2) {
if (listeners[i] == ActionListener.class) {
// lazily create the event:
if (_actionEvent == null) {
_actionEvent = new ActionEvent(
this, ActionEvent.ACTION_PERFORMED, _actionCommand);
}
((ActionListener)listeners[i+1]).actionPerformed(_actionEvent);
}
}
}
// documentation inherited from interface
public void contentsChanged (ListDataEvent e)
{
// if this update is informing us of a new selection, reflect that
// in the UI
int start = e.getIndex0(), count = start - e.getIndex1() + 1;
if (start == -1) {
// figure out the selected index
int selidx = -1;
Object eitem = _model.getSelectedItem();
if (eitem != null) {
int ecount = _model.getSize();
for (int i = 0; i < ecount; i++) {
if (eitem == _model.getElementAt(i)) {
selidx = i;
break;
}
}
}
// and update it
updateSelection(selidx);
} else {
// replace the buttons in this range
removeButtons(start, count);
addButtons(start, count);
}
}
// documentation inherited from interface
public void intervalAdded (ListDataEvent e)
{
int start = e.getIndex0(), count = e.getIndex1() - start + 1;
// adjust the selected index
if (_selectedIndex >= start) {
_selectedIndex += count;
}
// add buttons for the new interval
addButtons(start, count);
}
// documentation inherited from interface
public void intervalRemoved (ListDataEvent e)
{
// remove the buttons in the specified interval
int start = e.getIndex0(), count = e.getIndex1() - start + 1;
removeButtons(start, count);
SwingUtil.refresh(this);
}
// documentation inherited from interface
public void mouseClicked (MouseEvent e)
{
}
// documentation inherited from interface
public void mousePressed (MouseEvent e)
{
// ignore if we're not enabled
if (!isEnabled()) {
return;
}
// keep track of the selected button
_selectedButton = (JLabel)e.getSource();
// if the selected button is already selected, ignore the click
if (_selectedButton.getBorder() == SELECTED_BORDER) {
_selectedButton = null;
} else {
_selectedButton.setBorder(SELECTED_BORDER);
_selectedButton.repaint();
}
}
// documentation inherited from interface
public void mouseReleased (MouseEvent e)
{
// if the mouse was released within the bounds of the button, go
// ahead and select it properly
if (_selectedButton != null) {
if (_selectedButton.contains(e.getX(), e.getY())) {
// tell the model that the selection has changed (and
// we'll respond and do our business
Object elem = _selectedButton.getClientProperty("element");
_model.setSelectedItem(elem);
} else {
_selectedButton.setBorder(DESELECTED_BORDER);
_selectedButton.repaint();
}
// clear out the selected button indicator
_selectedButton = null;
}
}
// documentation inherited from interface
public void mouseEntered (MouseEvent e)
{
}
// documentation inherited from interface
public void mouseExited (MouseEvent e)
{
}
/**
* Adds buttons for the specified range of model elements.
*/
protected void addButtons (int start, int count)
{
Object selobj = _model.getSelectedItem();
for (int i = start; i < count; i++) {
Object elem = _model.getElementAt(i);
if (selobj == elem) {
_selectedIndex = i;
}
JLabel ibut = null;
if (elem instanceof Image) {
ibut = new JLabel(new ImageIcon((Image)elem));
} else {
ibut = new JLabel(elem.toString());
}
ibut.putClientProperty("element", elem);
ibut.addMouseListener(this);
ibut.setBorder((_selectedIndex == i) ?
SELECTED_BORDER : DESELECTED_BORDER);
add(ibut, i);
}
SwingUtil.refresh(this);
}
/**
* Removes the buttons in the specified interval.
*/
protected void removeButtons (int start, int count)
{
while (count-- > 0) {
remove(start);
}
// adjust the selected index
if (_selectedIndex >= start) {
if (start + count > _selectedIndex) {
_selectedIndex = -1;
} else {
_selectedIndex -= count;
}
}
}
/**
* Sets the selection to the specified index and updates the buttons
* to reflect the change. This does not update the model.
*/
protected void updateSelection (int selidx)
{
// do nothing if this element is already selected
if (selidx == _selectedIndex) {
return;
}
// unhighlight the old component
if (_selectedIndex != -1) {
JLabel but = (JLabel)getComponent(_selectedIndex);
but.setBorder(DESELECTED_BORDER);
}
// save the new selection
_selectedIndex = selidx;
// if the selection is valid, highlight the new component
if (_selectedIndex != -1) {
JLabel but = (JLabel)getComponent(_selectedIndex);
but.setBorder(SELECTED_BORDER);
}
// fire an action performed to let listeners know about our
// changed selection
fireActionPerformed();
repaint();
}
/** The contents of the box. */
protected ComboBoxModel _model;
/** The index of the selected button. */
protected int _selectedIndex = -1;
/** The button over which the mouse was pressed. */
protected JLabel _selectedButton;
/** Used when notifying our listeners. */
protected ActionEvent _actionEvent;
/** The action command we generate when the selection changes. */
protected String _actionCommand;
/** The border used for selected components. */
protected static final Border SELECTED_BORDER =
BorderFactory.createLoweredBevelBorder();
/** The border used for non-selected components. */
protected static final Border DESELECTED_BORDER =
BorderFactory.createRaisedBevelBorder();
}
@@ -0,0 +1,104 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import com.samskivert.swing.event.CommandEvent;
/**
* A button that fires CommandEvents when it is actioned.
*/
public class CommandButton extends JButton
{
/**
* Set the argument of the CommandEvents which we generate.
*/
public void setActionArgument (Object arg)
{
_argument = arg;
}
/**
* Get the argument that we'll use when we generate CommandEvents.
*/
public Object getActionArgument ()
{
return _argument;
}
@Override
protected void fireActionPerformed (ActionEvent event)
{
Object[] listeners = listenerList.getListenerList();
ActionEvent e = null;
// Process the listeners last to first, notfiying
// those that are interested in this event
for (int ii = listeners.length - 2; ii >= 0; ii -= 2) {
if (listeners[ii] == ActionListener.class) {
// Lazily create the event:
if (e == null) {
e = createEventToFire(event);
}
((ActionListener) listeners[ii + 1]).actionPerformed(e);
}
}
}
/**
* Create the event to fire from the prototype.
* If this method were broken out in AbstractButton, we wouldn't even
* have had to override fireActionPerformed().
*/
protected ActionEvent createEventToFire (ActionEvent proto)
{
String actionCommand = proto.getActionCommand();
if (actionCommand == null) {
actionCommand = getActionCommand();
}
Object arg = (proto instanceof CommandEvent)
? ((CommandEvent) proto).getArgument() : null;
// if we were passed an action event, or if it was a command event
// with a null arg...
if (arg == null) {
// ...use ours
arg = getActionArgument();
}
if (arg == null) {
// just create a plain ActionEvent
return new ActionEvent(CommandButton.this,
ActionEvent.ACTION_PERFORMED, actionCommand,
proto.getWhen(), proto.getModifiers());
} else {
// if we found an arg somewhere, create a CommandEvent
return new CommandEvent(CommandButton.this, actionCommand, arg,
proto.getWhen(), proto.getModifiers());
}
}
/** The CommandEvent argument for our actions. */
protected Object _argument;
}
@@ -0,0 +1,68 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import javax.swing.JPanel;
/**
* A controlled panel takes care of setting up its controller and
* providing it. When operating in conjunction with a controlled panel,
* the controller can automatically invoke {@link Controller#wasAdded} and
* {@link Controller#wasRemoved}.
*/
public abstract class ControlledPanel extends JPanel
implements ControllerProvider
{
/**
* Creates a controlled panel and its associated controller.
*/
public ControlledPanel ()
{
_controller = createController();
// let the controller know about this panel
if (_controller != null) {
_controller.setControlledPanel(this);
}
}
// documentation inherited from interface
public Controller getController ()
{
return _controller;
}
/**
* Called to create the controller associated with this controlled
* panel. Derived classes should override this and instantiate the
* appropriate controller derived class. Note that this will be called
* very early in the construction process, before derived classes have
* access to their member data and as such, the derived panel will not
* be able to pass anything interesting to its associated controller
* constructor. The expectation is that it will obtain a reference to
* its controller later in its own constructor and supply any extra
* initialization data that is needed at that time.
*/
protected abstract Controller createController ();
/** The controller with which we are working. */
protected Controller _controller;
}
@@ -0,0 +1,431 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.Method;
import javax.swing.AbstractButton;
import javax.swing.JButton;
import javax.swing.JComponent;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import com.samskivert.swing.event.CommandEvent;
import static com.samskivert.Log.log;
/**
* The controller class provides a basis for the separation of user interface
* code into display code and control code. The display code lives in a panel
* class (<code>javax.swing.JPanel</code> or something conceptually similar)
* and the control code lives in an associated controller class.
*
* <p> The controller philosophy is thus: The panel class (and its UI
* components) convert basic user interface actions into higher level actions
* that more cleanly encapsulate the action desired by the user and they pass
* those actions on to their controller. The controller then performs abstract
* processing based on the users desires and the changing state of the
* application and calls back to the panel to affect changes to the display.
*
* <p> Controllers also support the notion of scope. When a panel wishes to
* post an action, it doesn't do it directly to the controller. Instead it does
* it using a controller utility function called {@link #postAction}, which
* searches up the user interface hierarchy looking for a component that
* implements {@link ControllerProvider} which it will use to obtain the
* controller "in scope" for that component. That controller is requested to
* handle the action, but if it cannot handle the action, the next controller
* up the chain is located and requested to process the action.
*
* <p> In this manner, a hierarchy of controllers (often just two: one
* application wide and one for whatever particular mode the application is in
* at the moment) can provide a set of services that are available to all user
* interface elements in the entire application and in a way that doesn't
* require tight connectedness between the UI elements and the controllers.
*/
public abstract class Controller
implements ActionListener
{
/**
* This action listener can be wired up to any action event generator
* and it will take care of forwarding that event on to the controller
* in scope for the component that generated the action event.
*
* <p> For example, wiring a button up to a dispatcher would look like
* so:
*
* <pre>
* JButton button = new JButton("Do thing");
* button.setActionCommand("dothing");
* button.addActionListener(Controller.DISPATCHER);
* </pre>
*
* or, use the provided convenience function:
*
* <pre>
* JButton button =
* Controller.createActionButton("Do thing", "dothing");
* </pre>
*
* The controllers in scope would then be requested (in order) to
* process the <code>dothing</code> action whenever the button was
* clicked.
*/
public static final ActionListener DISPATCHER = new ActionListener() {
public void actionPerformed (ActionEvent event)
{
Controller.postAction(event);
}
};
/**
* Posts the specified action to the nearest controller in scope. The
* controller search begins with the source component of the action and
* traverses up the component tree looking for a controller to handle the
* action. The controller location and action event processing is
* guaranteed to take place on the AWT thread regardless of what thread
* calls <code>postAction</code> and that processing will not occur
* immediately but is instead appended to the AWT event dispatch queue for
* processing.
*/
public static void postAction (ActionEvent action)
{
// slip things onto the event queue for later
EventQueue.invokeLater(new ActionInvoker(action));
}
/**
* Like {@link #postAction(ActionEvent)} except that it constructs the
* action event for you with the supplied source component and string
* command. The <code>id</code> of the event will always be set to
* zero.
*/
public static void postAction (Component source, String command)
{
// slip things onto the event queue for later
ActionEvent event = new ActionEvent(source, 0, command);
EventQueue.invokeLater(new ActionInvoker(event));
}
/**
* Like {@link #postAction(ActionEvent)} except that it constructs a
* {@link CommandEvent} with the supplied source component, string
* command and argument.
*/
public static void postAction (
Component source, String command, Object argument)
{
// slip things onto the event queue for later
CommandEvent event = new CommandEvent(source, command, argument);
EventQueue.invokeLater(new ActionInvoker(event));
}
/**
* Creates a button and configures it with the specified label and action
* command and adds {@link #DISPATCHER} as an action listener.
*/
public static JButton createActionButton (String label, String command)
{
JButton button = new JButton(label);
configureAction(button, command);
return button;
}
/**
* Configures the supplied button with the {@link #DISPATCHER} action
* listener and the specified action command (which, if it is a method name
* will be looked up dynamically on the matching controller).
*/
public static void configureAction (AbstractButton button, String action)
{
button.setActionCommand(action);
button.addActionListener(DISPATCHER);
}
/**
* Lets this controller know about the panel that it is controlling.
*/
public void setControlledPanel (final JComponent panel)
{
panel.addHierarchyListener(new HierarchyListener() {
public void hierarchyChanged (HierarchyEvent e) {
boolean nowShowing = panel.isDisplayable();
//System.err.println("Controller." + Controller.this +
// " nowShowing=" + nowShowing +
// ", wasShowing=" + _showing);
if (_showing != nowShowing) {
_showing = nowShowing;
if (_showing) {
wasAdded();
} else {
wasRemoved();
}
}
}
boolean _showing = false;
});
}
/**
* Called when the panel controlled by this controller was added to
* the user interface hierarchy. This assumes that the controlled
* panel made itself known to the controller via {@link
* #setControlledPanel} (which is done automatically by {@link
* ControlledPanel} and derived classes).
*/
public void wasAdded ()
{
}
/**
* Called when the panel controlled by this controller was removed
* from the user interface hierarchy. This assumes that the controlled
* panel made itself known to the controller via {@link
* #setControlledPanel} (which is done automatically by {@link
* ControlledPanel} and derived classes).
*/
public void wasRemoved ()
{
}
/**
* Instructs this controller to process this action event. When an action
* is posted by a user interface element, it will be posted to the
* controller in closest scope for that element. If that controller handles
* the event, it should return true from this method to indicate that
* processing should stop. If it cannot handle the event, it can return
* false to indicate that the event should be propagated to the next
* controller up the chain.
*
* <p> This method will be called on the AWT thread, so the controller can
* safely manipulate user interface components while handling an action.
* However, this means that action handling cannot block and should not
* take an undue amount of time. If the controller needs to perform
* complicated, lengthy processing it should do so with a separate thread,
* for example via {@link com.samskivert.swing.util.TaskMaster}.
*
* <p> The default implementation of this method will reflect on the
* controller class, looking for a method that matches the name of the
* action event. For example, if the action was "exit" a method named
* "exit" would be sought. A handler method must provide one of three
* signatures: one accepting no arguments, one including only a reference
* to the source object, or one including the source object and an extra
* argument (which can be used only if the action event is an instance of
* {@link CommandEvent}). For example:
*
* <pre>
* public void cancelClicked (Object source);
* public void textEntered (Object source, String text);
* </pre>
*
* The arguments to the method can be as specific or as generic as desired
* and reflection will perform the appropriate conversions at runtime. For
* example, a method could be declared like so:
*
* <pre>
* public void cancelClicked (JButton source);
* </pre>
*
* One would have to ensure that the only action events generated with the
* action command string "cancelClicked" were generated by JButton
* instances if such a signature were used.
*
* @param action the action to be processed.
*
* @return true if the action was processed, false if it should be
* propagated up to the next controller in scope.
*/
public boolean handleAction (ActionEvent action)
{
Object arg = null;
if (action instanceof CommandEvent) {
arg = ((CommandEvent)action).getArgument();
}
return handleAction(action.getSource(), action.getActionCommand(), arg);
}
/**
* A version of {@link #handleAction(ActionEvent)} with the parameters
* broken out so that it can be used by non-Swing interface toolkits.
*/
public boolean handleAction (Object source, String action, Object arg)
{
Method method = null;
Object[] args = null;
try {
// look for the appropriate method
Method[] methods = getClass().getMethods();
int mcount = methods.length;
for (int i = 0; i < mcount; i++) {
if (methods[i].getName().equals(action) ||
// handle our old style of prepending "handle"
methods[i].getName().equals("handle" + action)) {
// see if we can generate the appropriate arguments
args = generateArguments(methods[i], source, arg);
// if we were able to, go ahead and use this method
if (args != null) {
method = methods[i];
break;
}
}
}
} catch (Exception e) {
log.warning("Error searching for action handler method", "controller", this,
"action", action);
return false;
}
try {
if (method != null) {
method.invoke(this, args);
return true;
} else {
return false;
}
} catch (Exception e) {
log.warning("Error invoking action handler", "controller", this, "action", action, e);
// even though we choked, we still "handled" the action
return true;
}
}
/**
* A convenience method for constructing and immediately handling an
* event on this controller (via {@link #handleAction(ActionEvent)}).
*
* @return true if the controller knew how to handle the action, false
* otherwise.
*/
public boolean handleAction (Component source, String command)
{
return handleAction(new ActionEvent(source, 0, command));
}
/**
* A convenience method for constructing and immediately handling an
* event on this controller (via {@link #handleAction(ActionEvent)}).
*
* @return true if the controller knew how to handle the action, false
* otherwise.
*/
public boolean handleAction (Component source, String command, Object arg)
{
return handleAction(new CommandEvent(source, command, arg));
}
// documentation inherited from interface ActionListener
public void actionPerformed (ActionEvent event)
{
handleAction(event);
}
/**
* Used by {@link #handleAction} to generate arguments to the action
* handler method.
*/
protected Object[] generateArguments (
Method method, Object source, Object argument)
{
// figure out what sort of arguments are required by the method
Class<?>[] atypes = method.getParameterTypes();
if (atypes == null || atypes.length == 0) {
return new Object[0];
} else if (atypes.length == 1) {
return new Object[] { source };
} else if (atypes.length == 2) {
if (argument != null) {
return new Object[] { source, argument };
}
log.warning("Unable to map argumentless event to handler method that requires an " +
"argument", "controller", this, "method", method, "source", source);
}
// we would have handled it, but we couldn't
return null;
}
/**
* This class is used to dispatch action events to controllers within
* the context of the AWT event dispatch mechanism.
*/
protected static class ActionInvoker implements Runnable
{
public ActionInvoker (ActionEvent action)
{
_action = action;
}
public void run ()
{
// do some sanity checking on the source
Object src = _action.getSource();
if (src == null || !(src instanceof Component)) {
log.warning("Requested to dispatch action on non-component source", "source", src,
"action", _action);
return;
}
// scan up the component hierarchy looking for a controller on
// which to dispatch this action
for (Component source = (Component)src; source != null;
source = source.getParent()) {
if (!(source instanceof ControllerProvider)) {
continue;
}
Controller ctrl = ((ControllerProvider)source).getController();
if (ctrl == null) {
log.warning("Provider returned null controller", "provider", source);
continue;
}
try {
// if the controller returns true, it handled the
// action and we can call this business done
if (ctrl.handleAction(_action)) {
return;
}
} catch (Exception e) {
log.warning("Controller choked on action", "ctrl", ctrl, "action", _action, e);
}
}
// if we got here, we didn't find a controller
log.warning("Unable to find a controller to process action", "action", _action);
}
protected ActionEvent _action;
}
}
@@ -0,0 +1,39 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
/**
* The controller provider interface is implemented by user interface
* elements that have an associated {@link
* com.samskivert.swing.Controller}. The hierarchy of the user interface
* elements defines the hierarchy of controllers and at any point in the
* UI element hierarchy, an element can implement controller provider and
* provide a controller that will process actions received at that scope
* or below.
*/
public interface ControllerProvider
{
/**
* Returns the controller to be used at the scope of the UI element
* that implements controller provider.
*/
public Controller getController ();
}
@@ -0,0 +1,67 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.awt.Dimension;
/**
* This record is used by the group layout managers to return a set of
* statistics computed for their target widgets.
*/
public class DimenInfo
{
public int count;
public int totwid;
public int tothei;
public int maxwid;
public int maxhei;
public int numfix;
public int fixwid;
public int fixhei;
public int maxfreewid;
public int maxfreehei;
public int totweight;
public Dimension[] dimens;
@Override
public String toString ()
{
StringBuilder buf = new StringBuilder();
buf.append("[count=").append(count);
buf.append(", totwid=").append(totwid);
buf.append(", tothei=").append(tothei);
buf.append(", maxwid=").append(maxwid);
buf.append(", maxhei=").append(maxhei);
buf.append(", numfix=").append(numfix);
buf.append(", fixwid=").append(fixwid);
buf.append(", fixhei=").append(fixhei);
buf.append(", maxfreewid=").append(maxfreewid);
buf.append(", maxfreehei=").append(maxfreehei);
buf.append(", totweight=").append(totweight);
return buf.append("]").toString();
}
}
@@ -0,0 +1,80 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.awt.AlphaComposite;
import java.awt.Component;
import java.awt.Composite;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.Icon;
/**
* Draws an icon with a specified alpha level.
*/
public class DimmedIcon implements Icon
{
/**
* Construct a dimmed icon that is drawn at 50% normal.
*/
public DimmedIcon (Icon icon)
{
this(icon, .5f);
}
/**
* Construct a dimmed icon that is drawn at the specified alpha level.
*/
public DimmedIcon (Icon icon, float alpha)
{
_alpha = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha);
_icon = icon;
}
// documentation inherited from interface Icon
public int getIconWidth ()
{
return _icon.getIconWidth();
}
// documentation inherited from interface Icon
public int getIconHeight ()
{
return _icon.getIconHeight();
}
// documentation inherited from interface Icon
public void paintIcon (Component c, Graphics g, int x, int y)
{
Graphics2D gfx = (Graphics2D) g;
Composite ocomp = gfx.getComposite();
gfx.setComposite(_alpha);
_icon.paintIcon(c, gfx, x, y);
gfx.setComposite(ocomp);
}
/** The icon we're actually drawing. */
protected Icon _icon;
/** Our alpha composite. */
protected Composite _alpha;
}
@@ -0,0 +1,97 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JComponent;
import static com.samskivert.Log.log;
/**
* Used to enable or disable a source component based on some
* asynchronously changing state.
*/
public class EnablingAdapter
{
/**
* Creates and returns an enabler that listens for changes in the
* specified property (which must be a {@link Boolean} valued
* property) and updates the target's enabled state accordingly.
*/
public static PropertyChangeListener getPropChangeEnabler (
String property, JComponent target, boolean invert)
{
return new PropertyChangeEnabler(property, target, invert);
}
/**
* Creates an enabling adapter with the specified target component.
*
* @param invert if true, the target component's enabled state is set
* to the inverse of the monitored state.
*/
protected EnablingAdapter (JComponent target, boolean invert)
{
_target = target;
_invert = invert;
}
/**
* Called by the appropriate derived adapter to adjust the target's
* enabled state.
*/
protected void stateChanged (boolean newState)
{
_target.setEnabled(_invert ? !newState : newState);
}
/** Used by {@link #getPropChangeEnabler}. */
protected static class PropertyChangeEnabler extends EnablingAdapter
implements PropertyChangeListener
{
public PropertyChangeEnabler (
String property, JComponent target, boolean invert)
{
super(target, invert);
_property = property;
}
public void propertyChange (PropertyChangeEvent evt)
{
if (evt.getPropertyName().equals(_property)) {
Object value = evt.getNewValue();
if (value instanceof Boolean) {
stateChanged(((Boolean)value).booleanValue());
} else {
log.warning("PropertyChangeEnabler connected to non-Boolean property",
"got", value);
}
}
}
protected String _property;
}
protected JComponent _target;
protected boolean _invert;
}
@@ -0,0 +1,462 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.awt.*;
import javax.swing.JPanel;
import java.util.HashMap;
/**
* Group layout managers lay out widgets in horizontal or vertical groups.
*/
public abstract class GroupLayout
implements LayoutManager2
{
/**
* The group layout managers supports two constraints: fixedness
* and weight. A fixed component will not be stretched along the major
* axis of the group. Those components that are stretched will have
* the extra space divided among them according to their weight
* (specifically receiving the ratio of their weight to the total
* weight of all of the free components in the container).
*
* To add a component with the fixed constraints, use the FIXED constant.
*/
public static class Constraints
{
/**
* Constructs a new constraints object with the specified weight,
* which is only applicable with the STRETCH policy.
*/
public Constraints (int weight) {
_weight = weight;
}
/**
* Is this Constraints specifying fixed?
*/
public final boolean isFixed () {
return (this == FIXED);
}
/**
* Get the weight.
*/
public final int getWeight () {
return _weight;
}
/**
* The weight of this component relative to the other components
* in the container. Only valid if the layout policy is STRETCH.
*/
protected int _weight = 1;
}
/** A class used to make our policy constants type-safe. */
public static class Policy
{
}
/** A class used to make our policy constants type-safe. */
public static class Justification
{
}
/**
* A constraints object that indicates that the component should be
* fixed and have the default weight of one. This is so commonly used
* that we create and make this object available here.
*/
public final static Constraints FIXED = new Constraints(Integer.MIN_VALUE);
/** Do not adjust the widgets on this axis. */
public final static Policy NONE = new Policy();
/** Stretch all the widgets to their maximum possible size on this axis. */
public final static Policy STRETCH = new Policy();
/** Stretch all the widgets to be equal to the size of the largest widget on this axis. */
public final static Policy EQUALIZE = new Policy();
/** Only valid for off-axis policy, this leaves widgets alone unless they are larger in the
* off-axis direction than their container, in which case it constrains them to fit on the
* off-axis. */
public final static Policy CONSTRAIN = new Policy();
/** A justification constant. */
public final static Justification CENTER = new Justification();
/** A justification constant. */
public final static Justification LEFT = new Justification();
/** A justification constant. */
public final static Justification RIGHT = new Justification();
/** A justification constant. */
public final static Justification TOP = new Justification();
/** A justification constant. */
public final static Justification BOTTOM = new Justification();
/** The default gap between components, in pixels. */
public static final int DEFAULT_GAP = 5;
public void setPolicy (Policy policy)
{
_policy = policy;
}
public Policy getPolicy ()
{
return _policy;
}
public void setOffAxisPolicy (Policy offpolicy)
{
_offpolicy = offpolicy;
}
public Policy getOffAxisPolicy ()
{
return _offpolicy;
}
public void setGap (int gap)
{
_gap = gap;
}
public int getGap ()
{
return _gap;
}
public void setJustification (Justification justification)
{
_justification = justification;
}
public Justification getJustification ()
{
return _justification;
}
public void setOffAxisJustification (Justification justification)
{
_offjust = justification;
}
public Justification getOffAxisJustification ()
{
return _offjust;
}
public void addLayoutComponent (String name, Component comp)
{
// nothing to do here
}
public void removeLayoutComponent (Component comp)
{
if (_constraints != null) {
_constraints.remove(comp);
}
}
public void addLayoutComponent (Component comp, Object constraints)
{
if (constraints != null) {
if (constraints instanceof Constraints) {
if (_constraints == null) {
_constraints = new HashMap<Component,Constraints>();
}
_constraints.put(comp, (Constraints)constraints);
} else {
throw new RuntimeException(
"GroupLayout constraints object must be of type GroupLayout.Constraints");
}
}
}
public float getLayoutAlignmentX (Container target)
{
// we don't support alignment like this
return 0f;
}
public float getLayoutAlignmentY (Container target)
{
// we don't support alignment like this
return 0f;
}
public Dimension minimumLayoutSize (Container parent)
{
return getLayoutSize(parent, MINIMUM);
}
public Dimension preferredLayoutSize (Container parent)
{
return getLayoutSize(parent, PREFERRED);
}
public Dimension maximumLayoutSize (Container parent)
{
return getLayoutSize(parent, MAXIMUM);
}
protected abstract Dimension getLayoutSize (Container parent, int type);
public abstract void layoutContainer (Container parent);
public void invalidateLayout (Container target)
{
// nothing to do here
}
/**
* Get the Constraints for the specified child component.
*
* @return a Constraints object, never null.
*/
protected Constraints getConstraints (Component child)
{
if (_constraints != null) {
Constraints c = _constraints.get(child);
if (c != null) {
return c;
}
}
return DEFAULT_CONSTRAINTS;
}
/**
* Computes dimensions of the children widgets that are useful for the
* group layout managers.
*/
protected DimenInfo computeDimens (Container parent, int type) {
int count = parent.getComponentCount();
DimenInfo info = new DimenInfo();
info.dimens = new Dimension[count];
for (int i = 0; i < count; i++) {
Component child = parent.getComponent(i);
if (!child.isVisible()) {
continue;
}
Dimension csize;
switch (type) {
case MINIMUM:
csize = child.getMinimumSize();
break;
case MAXIMUM:
csize = child.getMaximumSize();
break;
default:
csize = child.getPreferredSize();
break;
}
info.count++;
info.totwid += csize.width;
info.tothei += csize.height;
if (csize.width > info.maxwid) {
info.maxwid = csize.width;
}
if (csize.height > info.maxhei) {
info.maxhei = csize.height;
}
Constraints c = getConstraints(child);
if (c.isFixed()) {
info.fixwid += csize.width;
info.fixhei += csize.height;
info.numfix++;
} else {
info.totweight += c.getWeight();
if (csize.width > info.maxfreewid) {
info.maxfreewid = csize.width;
}
if (csize.height > info.maxfreehei) {
info.maxfreehei = csize.height;
}
}
info.dimens[i] = csize;
}
return info;
}
/**
* Creates a {@link JPanel} that is configured with a center-justified {@link HGroupLayout}
* with a configuration conducive to containing a row of buttons. Any supplied buttons are
* added to the box.
*/
public static JPanel makeButtonBox (Component... buttons)
{
return makeButtonBox(GroupLayout.CENTER, buttons);
}
/**
* Creates a {@link JPanel} that is configured with an {@link HGroupLayout} with a
* configuration conducive to containing a row of buttons. Any supplied buttons are added to
* the box.
*/
public static JPanel makeButtonBox (Justification justification, Component... buttons)
{
JPanel box = new JPanel(new HGroupLayout(NONE, justification));
for (Component button : buttons) {
box.add(button);
box.setOpaque(false);
}
return box;
}
/**
* Creates a {@link JPanel} that is configured with an {@link
* HGroupLayout} with the default configuration.
*/
public static JPanel makeHBox ()
{
return new JPanel(new HGroupLayout());
}
/**
* Creates a {@link JPanel} that is configured with an {@link
* HGroupLayout} with a configuration that stretches in both
* directions, with the specified gap.
*/
public static JPanel makeHStretchBox (int gap)
{
return new JPanel(new HGroupLayout(STRETCH, STRETCH, gap, CENTER));
}
/**
* Creates a {@link JPanel} that is configured with an {@link
* HGroupLayout} with the specified on-axis policy (default
* configuration otherwise).
*/
public static JPanel makeHBox (Policy policy)
{
return new JPanel(new HGroupLayout(policy));
}
/**
* Creates a {@link JPanel} that is configured with an {@link
* HGroupLayout} with the specified on-axis policy and justification
* (default configuration otherwise).
*/
public static JPanel makeHBox (Policy policy, Justification justification)
{
return new JPanel(new HGroupLayout(policy, justification));
}
/**
* Creates a {@link JPanel} that is configured with an {@link
* HGroupLayout} with the specified on-axis policy, justification and
* off-axis policy (default configuration otherwise).
*/
public static JPanel makeHBox (Policy policy, Justification justification,
Policy offAxisPolicy)
{
return new JPanel(new HGroupLayout(policy, offAxisPolicy,
DEFAULT_GAP, justification));
}
/**
* Creates a {@link JPanel} that is configured with an {@link
* VGroupLayout} with the default configuration.
*/
public static JPanel makeVBox ()
{
return new JPanel(new VGroupLayout());
}
/**
* Creates a {@link JPanel} that is configured with an {@link
* VGroupLayout} with the specified on-axis policy (default
* configuration otherwise).
*/
public static JPanel makeVBox (Policy policy)
{
return new JPanel(new VGroupLayout(policy));
}
/**
* Creates a {@link JPanel} that is configured with an {@link
* VGroupLayout} with the specified on-axis policy and justification
* (default configuration otherwise).
*/
public static JPanel makeVBox (Policy policy, Justification justification)
{
return new JPanel(new VGroupLayout(policy, justification));
}
/**
* Creates a {@link JPanel} that is configured with an {@link
* VGroupLayout} with the specified on-axis policy, justification and
* off-axis policy (default configuration otherwise).
*/
public static JPanel makeVBox (Policy policy, Justification justification,
Policy offAxisPolicy)
{
return new JPanel(new VGroupLayout(policy, offAxisPolicy,
DEFAULT_GAP, justification));
}
/**
* Creates a {@link JPanel} that is configured with an {@link
* VGroupLayout} with a configuration that stretches in both
* directions, with the specified gap.
*/
public static JPanel makeVStretchBox (int gap)
{
return new JPanel(new VGroupLayout(STRETCH, STRETCH, gap, CENTER));
}
protected Policy _policy = NONE;
protected Policy _offpolicy = CONSTRAIN;
protected int _gap = DEFAULT_GAP;
protected Justification _justification = CENTER;
protected Justification _offjust = CENTER;
protected HashMap<Component,Constraints> _constraints;
protected static final int MINIMUM = 0;
protected static final int PREFERRED = 1;
protected static final int MAXIMUM = 2;
/** All children added without a Constraints object are
* constrained by this Constraints object. */
protected static final Constraints DEFAULT_CONSTRAINTS = new Constraints(1);
}
@@ -0,0 +1,58 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class GroupLayoutTest
{
public static void main (String[] args)
{
JFrame frame = new JFrame("GroupLayoutTest");
JPanel panel = new JPanel();
GroupLayout layout = new HGroupLayout();
layout.setJustification(GroupLayout.CENTER);
layout.setPolicy(GroupLayout.STRETCH);
layout.setJustification(GroupLayout.RIGHT);
layout.setGap(15);
panel.setLayout(layout);
panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
JButton butone = new JButton("One");
panel.add(butone, GroupLayout.FIXED);
JButton buttwo = new JButton("Two");
panel.add(buttwo, GroupLayout.FIXED);
JButton butthree = new JButton("Three to get ready");
panel.add(butthree, GroupLayout.FIXED);
frame.addWindowListener(new WindowAdapter () {
@Override public void windowClosing (WindowEvent e) {
System.exit(0);
}
});
frame.getContentPane().add(panel, BorderLayout.CENTER);
frame.pack();
frame.setVisible(true);
}
}
@@ -0,0 +1,180 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.awt.*;
public class HGroupLayout extends GroupLayout
{
public HGroupLayout (Policy policy, Policy offpolicy, int gap, Justification justification)
{
_policy = policy;
_offpolicy = offpolicy;
_gap = gap;
_justification = justification;
}
public HGroupLayout (Policy policy, int gap, Justification justification)
{
_policy = policy;
_gap = gap;
_justification = justification;
}
public HGroupLayout (Policy policy, Justification justification)
{
_policy = policy;
_justification = justification;
}
public HGroupLayout (Policy policy)
{
_policy = policy;
}
public HGroupLayout ()
{
}
@Override
protected Dimension getLayoutSize (Container parent, int type)
{
DimenInfo info = computeDimens(parent, type);
Dimension dims = new Dimension();
if (_policy == STRETCH) {
dims.width = info.maxfreewid * (info.count - info.numfix) + info.fixwid;
} else if (_policy == EQUALIZE) {
dims.width = info.maxwid * info.count;
} else { // NONE or CONSTRAIN
dims.width = info.totwid;
}
dims.width += (info.count - 1) * _gap;
dims.height = info.maxhei;
// account for the insets
Insets insets = parent.getInsets();
dims.width += insets.left + insets.right;
dims.height += insets.top + insets.bottom;
return dims;
}
@Override
public void layoutContainer (Container parent)
{
Rectangle b = parent.getBounds();
DimenInfo info = computeDimens(parent, PREFERRED);
// adjust the bounds width and height to account for the insets
Insets insets = parent.getInsets();
b.width -= (insets.left + insets.right);
b.height -= (insets.top + insets.bottom);
int nk = parent.getComponentCount();
int sx, sy;
int totwid, totgap = _gap * (info.count-1);
int freecount = info.count - info.numfix;
// when stretching, there is the possibility that a pixel or more
// will be lost to rounding error. we account for that here and
// assign the extra space to the first free component
int freefrac = 0;
// do the on-axis policy calculations
int defwid = 0;
if (_policy == STRETCH) {
if (freecount > 0) {
int freewid = b.width - info.fixwid - totgap;
defwid = freewid / info.totweight;
freefrac = freewid % info.totweight;
totwid = b.width;
} else {
totwid = info.fixwid + totgap;
}
} else if (_policy == EQUALIZE) {
defwid = info.maxwid;
totwid = info.fixwid + defwid * freecount + totgap;
} else { // NONE or CONSTRAIN
totwid = info.totwid + totgap;
}
// do the off-axis policy calculations
int defhei = 0;
if (_offpolicy == STRETCH) {
defhei = b.height;
} else if (_offpolicy == EQUALIZE) {
defhei = info.maxhei;
}
// do the justification-related calculations
if (_justification == LEFT || _justification == TOP) {
sx = insets.left;
} else if (_justification == CENTER) {
sx = insets.left + (b.width - totwid)/2;
} else { // RIGHT or BOTTOM
sx = insets.left + b.width - totwid;
}
// do the layout
for (int i = 0; i < nk; i++) {
// skip non-visible kids
if (info.dimens[i] == null) {
continue;
}
Component child = parent.getComponent(i);
Constraints c = getConstraints(child);
int newwid, newhei;
if (_policy == NONE || c.isFixed()) {
newwid = info.dimens[i].width;
} else {
newwid = freefrac + ((_policy == STRETCH) ? defwid * c.getWeight() : defwid);
// clear out the extra pixels the first time they're used
freefrac = 0;
}
if (_offpolicy == NONE) {
newhei = info.dimens[i].height;
} else if (_offpolicy == CONSTRAIN) {
newhei = Math.min(info.dimens[i].height, b.height);
} else {
newhei = defhei;
}
// determine our off-axis position
if (_offjust == LEFT || _offjust == TOP) {
sy = insets.top;
} else if (_offjust == RIGHT || _offjust == BOTTOM) {
sy = insets.top + b.height - newhei;
} else { // CENTER
sy = insets.top + (b.height - newhei)/2;
}
child.setBounds(sx, sy, newwid, newhei);
sx += child.getSize().width + _gap;
}
}
}
@@ -0,0 +1,346 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.awt.EventQueue;
import java.text.NumberFormat;
import java.text.ParseException;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;
import javax.swing.text.PlainDocument;
/**
* A text field that will only allow editing of integer values within
* the specified range.
*/
public class IntField extends JTextField
{
/**
* Create an IntField with the range 0 - Integer.MAX_VALUE, with
* 0 as the initial value.
*/
public IntField ()
{
this(Integer.MAX_VALUE);
}
/**
* Create an IntField with the specified maximum.
*/
public IntField (int maxValue)
{
this(0, maxValue);
}
/**
* Create an IntField with the specified minimum and maximum, with
* the minimum value initially displayed.
*/
public IntField (int minValue, int maxValue)
{
this(minValue, minValue, maxValue);
}
/**
* Create an IntField with the specified initial, minimum, and
* maximum values.
*/
public IntField (int initial, int minValue, int maxValue)
{
super(new IntDocument(), format(initial), 5);
validateMinMax(minValue, maxValue);
if (initial > maxValue || initial < minValue) {
throw new IllegalArgumentException("initial value not between " +
"min and max");
}
_minValue = minValue;
_maxValue = maxValue;
setHorizontalAlignment(JTextField.RIGHT);
// create a document filter which enforces the restrictions on
// what text may be entered. This is similar to the filter
// that is configured in SwingUtil.setDocumentHelpers, only the
// remove operation is modified to do the sneaky highlighting we do.
final IntDocument doc = (IntDocument) getDocument();
doc.setDocumentFilter(new DocumentFilter() {
@Override public void remove (FilterBypass fb, int offset, int length)
throws BadLocationException
{
String current = doc.getText(0, doc.getLength());
String potential = current.substring(0, offset) +
current.substring(offset + length);
// if the bit to be removed is still a valid value, go ahead
if (unformatSafe(potential) >= _minValue) {
transform(fb, current, potential);
return;
}
// otherwise, we are going to highlight the bit instead
// of removing it
int selStart = getSelectionStart();
if (selStart > 0) {
// see if the next highlighted character is a digit
char replace = current.charAt(selStart - 1);
if (Character.isDigit(replace)) {
// and can be zeroed out
String zeroed = current.substring(0, selStart - 1) +
"0" + current.substring(selStart);
// if so, change it to a zero
if (unformatSafe(zeroed) >= _minValue) {
fb.replace(selStart - 1, 1, "0", null);
} else {
// otherwise, re-set the entire field to
// contain the minimum value, and highlight it
String min = format(_minValue);
fb.replace(0, current.length(), min, null);
setSelectionStart(0);
setSelectionEnd(min.length());
return;
}
}
}
// grow the selection to encompass one more character
setSelectionStart(selStart - 1);
}
@Override public void insertString (FilterBypass fb, int offset,
String s, AttributeSet attr)
throws BadLocationException
{
String current = doc.getText(0, doc.getLength());
String potential = current.substring(0, offset) + s +
current.substring(offset);
transform(fb, current, potential);
}
@Override public void replace (FilterBypass fb, int offset, int length,
String text, AttributeSet attrs)
throws BadLocationException
{
String current = doc.getText(0, doc.getLength());
String potential = current.substring(0, offset) + text +
current.substring(offset + length);
transform(fb, current, potential);
}
protected void transform (FilterBypass fb,
String current, String potential)
throws BadLocationException
{
boolean wouldaBeenEqual = current.equals(potential);
potential = transform(potential);
boolean selection = (getSelectionEnd() != getSelectionStart());
// we only change it if it needs changing
if (!current.equals(potential) ||
// or if it would have been the same pre-transforming
// and there is a selection (IE undo the selection)
(wouldaBeenEqual && selection)) {
if (selection) {
// undo the selection to not cause an exception
setCaretPosition(0);
}
fb.replace(0, doc.getLength(), potential, null);
}
}
/**
* Ensure that the specified text is formatted and within
* the bounds.
*/
protected String transform (String text)
{
// if we're at least the minvalue, everything's ok
int val = unformatSafe(text);
if (val >= _minValue) {
return format(Math.min(_maxValue, val));
}
// otherwise, see if we can append zeros and make a valid value,
// remembering how many digits we added
int newVal = val;
int digits = 0;
if (newVal > 0) {
while (newVal * 10 <= _maxValue) {
newVal *= 10;
digits++;
if (newVal >= _minValue) {
break;
}
}
}
// if that didn't work, just set it to the min value and
// highlight all the digits
if (newVal < _minValue) {
newVal = _minValue;
digits = String.valueOf(newVal).length();
}
// return the new value, but post an event to immediately
// highlight the digits that the user did not enter, so
// that they can type over them
final String newText = format(newVal);
final int fdigits = digits;
EventQueue.invokeLater(new Runnable() {
public void run () {
String text = getText();
if (text.equals(newText)) {
int len = text.length();
setSelectionEnd(len);
int digits = fdigits;
for (int ii = len - 1; ii >= 0; ii--) {
if (Character.isDigit(text.charAt(ii))) {
--digits;
if (digits == 0) {
setSelectionStart(ii);
break;
}
}
}
}
}
});
return newText;
}
});
}
/**
* Validate min/max.
*/
protected void validateMinMax (int minValue, int maxValue)
{
if (minValue < 0) {
throw new IllegalArgumentException("minValue must be at least 0");
}
if (maxValue < minValue) {
throw new IllegalArgumentException(
"maxValue must be greater than minValue");
}
}
/**
* Return the int that is represented by this field.
*/
public int getValue ()
{
return unformatSafe(getText());
}
/**
* Set the text to the value specified.
*/
public void setValue (int value)
{
setText(format(value));
}
/**
* Change the current min value.
*/
public void setMinValue (int minValue)
{
validateMinMax(minValue, _maxValue);
_minValue = minValue;
validateText();
}
/**
* Change the current max value.
*/
public void setMaxValue (int maxValue)
{
validateMinMax(_minValue, maxValue);
_maxValue = maxValue;
validateText();
}
/**
* Ensure that the value we're displaying is between the minimum and
* the maximum.
*/
protected void validateText ()
{
setText(getText());
}
/**
* Format the specified monetary value into a string.
* This just puts commas in.
*/
public static String format (int value)
{
return _formatter.format(value);
}
/**
* Parse numbers, with commas being ok.
*/
public static int unformat (String text)
throws ParseException
{
return _formatter.parse(text).intValue();
}
/**
* Parse numbers, don't throw exceptions.
*/
public static int unformatSafe (String text)
{
try {
return unformat(text);
} catch (ParseException pe) {
return 0;
}
}
/**
* Our own special Document class.
*/
protected static class IntDocument extends PlainDocument
{
@Override protected void fireRemoveUpdate (DocumentEvent e)
{
// suppress: the DocumentFilter implements replace by doing
// a remove and then an insert. We do not want listeners to ever
// be notified when the document contains invalid text.
// (Gosh, it'd be nice if we didn't have to hack this, but
// the standard implementation has a bunch of methods with
// package-protected access. Thanks Sun!)
}
}
/** min/max */
protected int _minValue, _maxValue;
/** Formats and parses numbers with commas in them. */
protected static final NumberFormat _formatter = NumberFormat.getIntegerInstance();
}
@@ -0,0 +1,59 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.text.NumberFormat;
import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;
/**
* A table cell renderer that that formats integers according to the
* locale's desires.
*/
public class IntegerTableCellRenderer extends DefaultTableCellRenderer
{
public IntegerTableCellRenderer ()
{
setHorizontalAlignment(RIGHT);
}
@Override
protected void setValue (Object value)
{
if ((value instanceof Integer) || (value instanceof Long)) {
setText(_nfi.format(value) + " ");
} else {
super.setValue(value);
}
}
// our number formatter
protected NumberFormat _nfi = NumberFormat.getIntegerInstance();
/**
* A convenience method for installing this renderer.
*/
public static void install (JTable table)
{
table.setDefaultRenderer(Number.class, new IntegerTableCellRenderer());
}
}
@@ -0,0 +1,101 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.awt.Component;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JLayeredPane;
/**
* Used for displaying dialogs internally. Be sure to use {@link
* #showDialog} and {@link #dismissDialog} to show and hide these dialogs
* because they need to do some extra fiddling that the regular show and
* hide wouldn't do.
*/
public class JInternalDialog extends JInternalFrame
{
/**
* Creates a dialog that will display itself in the layered pane of
* the frame that contains the supplied component.
*/
public JInternalDialog (JComponent friend)
{
this(JLayeredPane.getLayeredPaneAbove(friend));
}
/**
* Creates a dialog that will display itself in the layered pane of
* the supplied frame.
*/
public JInternalDialog (JFrame frame)
{
this(frame.getLayeredPane());
}
/**
* Creates a dialog that will display itself in the specified layered
* pane.
*/
public JInternalDialog (JLayeredPane parent)
{
_parent = parent;
}
/**
* Adds this dialog to its parent and shows it.
*/
public void showDialog ()
{
_parent.add(this, JLayeredPane.PALETTE_LAYER);
setVisible(true);
}
/**
* Hides this dialog and removes it from its parent.
*/
public void dismissDialog ()
{
setVisible(false);
_parent.remove(this);
}
/**
* Scans up the interface hierarchy looking for the {@link
* JInternalDialog} that contains the supplied child component and
* dismisses it.
*/
public static void dismissDialog (Component child)
{
if (child == null) {
return;
} else if (child instanceof JInternalDialog) {
((JInternalDialog)child).dismissDialog();
} else {
dismissDialog(child.getParent());
}
}
/** Our parent. */
protected JLayeredPane _parent;
}
@@ -0,0 +1,789 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2010 Michael Bayne, et al.
//
// This library 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 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.swing;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.TextLayout;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.geom.Rectangle2D;
import java.text.AttributedString;
import java.text.AttributedCharacterIterator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.SwingConstants;
import com.samskivert.util.RunAnywhere;
import com.samskivert.util.StringUtil;
import com.samskivert.util.Tuple;
import com.samskivert.swing.util.SwingUtil;
import static com.samskivert.Log.log;
/**
* The label is a multipurpose text display mechanism that can display small amounts of text
* wrapped to fit into a variety of constrained spaces. It can be requested to conform to a
* particular width or height and will expand into the other dimension in order to accomodate the
* text at hand. It is not a component, but is intended for use by components and other more
* heavyweight entities.
*/
public class Label implements SwingConstants, LabelStyleConstants
{
/** The pattern used to mark the start/end of color blocks. */
public static final Pattern COLOR_PATTERN = Pattern.compile("#([Xx]|[0-9A-Fa-f]{6}+)");
/**
* Filter out any color tags from the specified text.
*/
public static String filterColors (String txt)
{
if (txt == null) return null;
return COLOR_PATTERN.matcher(txt).replaceAll("");
}
/**
* Escape any special tags so that they won't be interpreted by the label.
*/
public static String escapeColors (String txt)
{
if (txt == null) return null;
return COLOR_PATTERN.matcher(txt).replaceAll("#''$1");
}
/**
* Un-escape special tags so that they again look correct. Called by rendering components that
* do not understand the color tags to filter colors and unescape any escaped colors.
*/
public static String unescapeColors (String txt)
{
return unescapeColors(filterColors(txt), true);
}
/**
* Un-escape escaped tags so that they look as the users intended.
*/
private static String unescapeColors (String txt, boolean restore)
{
if (txt == null) return null;
String prefix = restore ? "#" : "%";
return ESCAPED_PATTERN.matcher(txt).replaceAll(prefix + "$1");
}
/**
* Constructs a blank label.
*/
public Label ()
{
this("");
}
/**
* Constructs a label with the supplied text.
*/
public Label (String text)
{
this(text, null, null);
}
/**
* Constructs a label with the supplied text and configuration parameters.
*/
public Label (String text, Color textColor, Font font)
{
this(text, NORMAL, textColor, null, font);
}
/**
* Constructs a label with the supplied text and configuration parameters.
*/
public Label (String text, int style, Color textColor, Color altColor, Font font)
{
setText(text);
setStyle(style);
setTextColor(textColor);
setAlternateColor(altColor);
setFont(font);
}
/**
* Returns the text displayed by this label.
*/
public String getText ()
{
return _text;
}
/**
* Sets the text to be displayed by this label.
*
* <p> This should be followed by a call to {@link #layout} before a call is made to {@link
* #render} as this method invalidates the layout information.
*
* @return true if the text changed as a result of being set, false if the label was already
* displaying the requested text.
*/
public boolean setText (String text)
{
// the Java text stuff freaks out in a variety of ways if it is asked to deal with the
// empty string, so we fake blank labels by just using a space
if (StringUtil.isBlank(text)) {
text = " ";
}
// if there is no change then avoid doing anything
if (text.equals((_rawText == null) ? _text : _rawText)) {
return false;
}
// _text should contain the text without any tags
_text = filterColors(text);
// _rawText will be null if there are no tags
_rawText = text.equals(_text) ? null : text;
// if what we were passed contains escaped color tags, unescape them
_text = unescapeColors(_text, true);
if (_rawText != null) {
_rawText = unescapeColors(_rawText, false);
}
invalidate("setText");
return true;
}
/**
* Sets the font to be used by this label. If the font is not set, the current font of the
* graphics context will be used.
*
* <p> This should be followed by a call to {@link #layout} before a call is made to {@link
* #render} as this method invalidates the layout information.
*/
public void setFont (Font font)
{
_font = font;
invalidate("setFont");
}
/**
* Returns the color used to render the text.
*/
public Color getTextColor ()
{
return _textColor;
}
/**
* Sets the color used to render the text. Setting the text color to <code>null</code> will
* render the label in the graphics context color (which is the default).
*/
public void setTextColor (Color color)
{
_textColor = color;
}
/**
* Returns the alternate color used to render the text's outline or shadow, if any.
*/
public Color getAlternateColor ()
{
return _alternateColor;
}
/**
* Instructs the label to render the text with the specified alternate color when
* rendering. The text itself will be rendered in whatever color is currently set in the
* graphics context, but the outline or shadow (if any) will always be in the specified color.
*/
public void setAlternateColor (Color color)
{
_alternateColor = color;
}
/**
* Returns the alignment of the text within the label.
*/
public int getAlignment ()
{
return _align;
}
/**
* Sets the alignment of the text within the label to either {@link SwingConstants#LEFT},
* {@link SwingConstants#RIGHT}, or {@link SwingConstants#CENTER}. The default alignment is
* selected to be appropriate for the locale of the text being rendered.
*
* <p> This should be followed by a call to {@link #layout} before a call is made to {@link
* #render} as this method invalidates the layout information.
*/
public void setAlignment (int align)
{
_align = align;
}
/**
* Sets the style of the text within the label to one of the styles defined in {@link
* LabelStyleConstants}. Some styles can be combined together into a mask,
* ie. <code>BOLD|UNDERLINE</code>.
*
* <p> This should be followed by a call to {@link #layout} before a call is made to {@link
* #render} as this method invalidates the layout information.
*/
public void setStyle (int style)
{
_style = style;
invalidate("setStyle");
}
/**
* Instructs the label to attempt to achieve a balance between width and height that
* approximates the golden ratio (width ~1.618 times height).
*
* <p> This should be followed by a call to {@link #layout} before a call is made to {@link
* #render} as this method invalidates the layout information.
*/
public void setGoldenLayout ()
{
// use -1 as an indicator that we should be golden
_constraints.width = -1;
_constraints.height = -1;
invalidate("setGoldenLayout");
}
/**
* Sets the target width for this label. Text will be wrapped to fit into this width, forcibly
* breaking words on character boundaries if a single word is too long to fit into the target
* width. Calling this method will annul any previously established target height as we must
* have one degree of freedom in which to maneuver.
*
* <p> This should be followed by a call to {@link #layout} before a call is made to {@link
* #render} as this method invalidates the layout information.
*/
public void setTargetWidth (int targetWidth)
{
if (targetWidth <= 0) {
throw new IllegalArgumentException(
"Invalid target width '" + targetWidth + "'");
}
_constraints.width = targetWidth;
_constraints.height = 0;
invalidate("setTargetWidth");
}
/**
* Sets the target height for this label. A simple algorithm will be used to balance the width
* of the text in order that there are only as many lines of text as fit into the target
* height. If rendering the label as a single line of text causes it to be taller than the
* target height, we simply render ourselves anyway. Calling this method will annul any
* previously established target width as we must have one degree of freedom in which to
* maneuver.
*
* <p> This should be followed by a call to {@link #layout} before a call is made to {@link
* #render} as this method invalidates the layout information.
*/
public void setTargetHeight (int targetHeight)
{
if (targetHeight <= 0) {
throw new IllegalArgumentException(
"Invalid target height '" + targetHeight + "'");
}
_constraints.width = 0;
_constraints.height = targetHeight;
invalidate("setTargetHeight");
}
/**
* Clears out previously configured target dimensions for this label.
*/
public void clearTargetDimens ()
{
_constraints.width = 0;
_constraints.height = 0;
}
/**
* Returns the number of lines used by this label.
*/
public int getLineCount ()
{
return _layouts.length;
}
/**
* Returns our computed dimensions. Only valid after a call to {@link #layout}.
*/
public Dimension getSize ()
{
return _size;
}
/**
* Returns true if this label has been laid out, false if not.
*/
public boolean isLaidOut ()
{
return (_layouts != null);
}
/**
* Calls {@link #layout(Graphics2D)} with the graphics context for the given component.
*/
public void layout (Component comp)
{
Graphics2D gfx = (Graphics2D)comp.getGraphics();
if (gfx != null) {
layout(gfx);
gfx.dispose();
}
}
/**
* Requests that this label lay out its text, obtaining information from the supplied graphics
* context to do so. It is expected that the label will be subsequently rendered in the same
* graphics context or at least one that is configured very similarly. If not, wackiness may
* ensue.
*/
public void layout (Graphics2D gfx)
{
// if text antialiasing is enabled by default, honor that setting
Object oalias = SwingUtil.getDefaultTextAntialiasing() ?
SwingUtil.activateAntiAliasing(gfx) : null;
// now we can get our font render context (antialias settings are part of our context)
FontRenderContext frc = gfx.getFontRenderContext();
List<Tuple<TextLayout,Rectangle2D>> layouts = null;
// if we have a target height, do some processing and convert that into a target width
if (_constraints.height > 0 || _constraints.width == -1) {
int targetHeight = _constraints.height;
// if we're approximating the golden ratio, target a height that gets us near that
// ratio, then we can err on the side of being a bit wider which is generally nicer
// than being taller (for those of us that don't speak verticall written languages,
// anyway)
if (_constraints.width == -1) {
TextLayout layout = new TextLayout(textIterator(gfx), frc);
Rectangle2D bounds = getBounds(layout);
int lines = 1;
double width = getWidth(bounds)/lines;
double height = getHeight(layout)*lines;
double delta = Math.abs(width/height - GOLDEN_RATIO);
do {
width = getWidth(bounds) / (lines+1);
double nheight = getHeight(layout) * (lines+1);
double ndelta = Math.abs(width/nheight - GOLDEN_RATIO);
if (delta <= ndelta) {
break;
}
delta = ndelta;
height = nheight;
} while (++lines < 200); // cap ourselves at 200 lines
targetHeight = (int)Math.ceil(height);
}
TextLayout layout = new TextLayout(textIterator(gfx), frc);
Rectangle2D bounds = getBounds(layout);
int lines = Math.round(targetHeight / getHeight(layout));
if (lines > 1) {
int targetWidth = (int)Math.round(getWidth(bounds) / lines);
// attempt to lay the text out in the specified width, incrementing by 10% each
// time; limit our attempts to 10 expansions to avoid infinite loops if something
// is fucked
for (int i = 0; i < 10; i++) {
LineBreakMeasurer measurer = new LineBreakMeasurer(textIterator(gfx), frc);
layouts = computeLines(measurer, targetWidth, _size, true);
if ((layouts != null) && (layouts.size() <= lines)) {
break;
}
targetWidth = (int)Math.round(targetWidth * 1.1);
}
}
} else if (_constraints.width > 0) {
LineBreakMeasurer measurer =
new LineBreakMeasurer(textIterator(gfx), frc);
layouts = computeLines(measurer, _constraints.width, _size, false);
}
// if no constraint, or our constraining height puts us on one line then layout on one line
// and call it good
if (layouts == null) {
TextLayout layout = new TextLayout(textIterator(gfx), frc);
Rectangle2D bounds = getBounds(layout);
// for some reason JDK1.3 on Linux chokes on setSize(double,double)
_size.setSize(Math.ceil(getWidth(bounds)), Math.ceil(getHeight(layout)));
layouts = new ArrayList<Tuple<TextLayout,Rectangle2D>>();
layouts.add(new Tuple<TextLayout,Rectangle2D>(layout, bounds));
}
// create our layouts array
int lcount = layouts.size();
_layouts = new TextLayout[lcount];
_lbounds = new Rectangle2D[lcount];
_leaders = new float[lcount];
for (int ii = 0; ii < lcount; ii++) {
Tuple<TextLayout,Rectangle2D> tup = layouts.get(ii);
_layouts[ii] = tup.left;
_lbounds[ii] = tup.right;
// account for potential leaders
if (_lbounds[ii].getX() < 0) {
_leaders[ii] = (float)-_lbounds[ii].getX();
}
}
// finally restore our antialiasing state
SwingUtil.restoreAntiAliasing(gfx, oalias);
}
/**
* Computes the lines of text for this label given the specified target width. The overall size
* of the computed lines is stored into the <code>size</code> parameter.
*
* @return an {@link List} or null if <code>keepWordsWhole</code> was true and the lines could
* not be layed out in the target width.
*/
protected List<Tuple<TextLayout,Rectangle2D>> computeLines (
LineBreakMeasurer measurer, int targetWidth, Dimension size, boolean keepWordsWhole)
{
// start with a size of zero
double width = 0, height = 0;
List<Tuple<TextLayout,Rectangle2D>> layouts = new ArrayList<Tuple<TextLayout,Rectangle2D>>();
try {
// obtain our new dimensions by using a line break iterator to lay out our text one
// line at a time
TextLayout layout;
int lastposition = _text.length();
while (true) {
int nextret = _text.indexOf('\n', measurer.getPosition() + 1);
if (nextret == -1) {
nextret = lastposition;
}
layout = measurer.nextLayout(targetWidth, nextret, keepWordsWhole);
if (layout == null) {
break;
}
Rectangle2D bounds = getBounds(layout);
width = Math.max(width, getWidth(bounds));
height += getHeight(layout);
layouts.add(new Tuple<TextLayout,Rectangle2D>(layout, bounds));
}
// fill in the computed size; for some reason JDK1.3 on Linux chokes on
// setSize(double,double)
size.setSize(Math.ceil(width), Math.ceil(height));
// this can only happen if keepWordsWhole is true
if (measurer.getPosition() < lastposition) {
return null;
}
} catch (Throwable t) {
log.warning("Label layout failed", "text", _text, t);
}
return layouts;
}
/**
* Renders the layout at the specified position in the supplied graphics context.
*/
public void render (Graphics2D gfx, float x, float y)
{
// nothing to do if we haven't been laid out
if (_layouts == null) {
log.warning(hashCode() + " Unlaid-out label asked to render", "text", _text
/*, "last", _invalidator */);
return;
}
Color old = gfx.getColor();
if (_textColor != null) {
gfx.setColor(_textColor);
}
// We're going to do a couple things differently if we're antialiased
RenderingHints hints = gfx.getRenderingHints();
boolean antialiased = hints.containsKey(RenderingHints.KEY_ANTIALIASING) ||
hints.containsKey(RenderingHints.KEY_TEXT_ANTIALIASING);
// render our text
for (int i = 0; i < _layouts.length; i++) {
TextLayout layout = _layouts[i];
Rectangle2D lbounds = _lbounds[i];
y += layout.getAscent();
float extra = (float)Math.floor(_size.width - getWidth(lbounds));
float rx;
switch (_align) {
case -1: rx = x + (layout.isLeftToRight() ? 0 : extra); break;
default:
case LEFT: rx = x; break;
case RIGHT: rx = x + extra; break;
case CENTER: rx = x + extra/2; break;
}
// shift over any lines that start with a font that extends into negative x-land
rx += _leaders[i];
// System.out.println(i + " x: " + x + " y: " + y + " rx: " + rx + " a: " + _align +
// " width: " + _size.width + " lx: " + lbounds.getX() +
// " lwidth: " + getWidth(lbounds) + " extra: " + extra);
Color textColor;
if ((_style & OUTLINE) != 0) {
// render the outline using the hacky, but much nicer than using "real" outlines
// (via TextLayout.getOutline), method
textColor = gfx.getColor();
gfx.setColor(_alternateColor);
_mainDraw = false;
if (antialiased) {
// We need to fill in the actual spot we'll be drawing on top of if antialiased
layout.draw(gfx, rx + 1, y + 1);
}
layout.draw(gfx, rx, y);
layout.draw(gfx, rx, y + 1);
layout.draw(gfx, rx, y + 2);
layout.draw(gfx, rx + 1, y);
layout.draw(gfx, rx + 1, y + 2);
layout.draw(gfx, rx + 2, y);
layout.draw(gfx, rx + 2, y + 1);
layout.draw(gfx, rx + 2, y + 2);
_mainDraw = true;
gfx.setColor(textColor);
layout.draw(gfx, rx + 1, y + 1);
if (antialiased) {
// If antialiased, draw a second time to make sure our letters are nice and
// solid looking on top of the outline
layout.draw(gfx, rx + 1, y + 1);
}
} else if ((_style & SHADOW) != 0) {
textColor = gfx.getColor();
gfx.setColor(_alternateColor);
_mainDraw = false;
layout.draw(gfx, rx, y + 1);
_mainDraw = true;
gfx.setColor(textColor);
layout.draw(gfx, rx + 1, y);
} else if ((_style & BOLD) != 0) {
_mainDraw = false;
layout.draw(gfx, rx, y);
_mainDraw = true;
layout.draw(gfx, rx + 1, y);
} else {
layout.draw(gfx, rx, y);
}
y += layout.getDescent() + layout.getLeading();
}
gfx.setColor(old);
}
/**
* Constructs an attributed character iterator with our text and the appropriate font.
*/
protected AttributedCharacterIterator textIterator (Graphics2D gfx)
{
// first set up any attributes that apply to the entire text
Font font = (_font == null) ? gfx.getFont() : _font;
HashMap<TextAttribute,Object> map = new HashMap<TextAttribute,Object>();
map.put(TextAttribute.FONT, font);
if ((_style & UNDERLINE) != 0) {
map.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_LOW_ONE_PIXEL);
}
AttributedString text = new AttributedString(_text, map);
addAttributes(text);
return text.getIterator();
}
/**
* Add any attributes to the text.
*/
protected void addAttributes (AttributedString text)
{
// add any color attributes for specific segments
if (_rawText != null) {
Matcher m = COLOR_PATTERN.matcher(_rawText);
int startSeg = 0, endSeg = 0;
Color lastColor = null;
while (m.find()) {
// color the segment just passed
endSeg += m.start();
if (lastColor != null) {
text.addAttribute(TextAttribute.FOREGROUND, lastColor, startSeg, endSeg);
}
// parse the tag: start or end a color
String group = m.group(1);
if ("x".equalsIgnoreCase(group)) {
lastColor = null;
} else {
lastColor = new Color(Integer.parseInt(group, 16));
}
// prepare for the next segment
startSeg = endSeg;
// Subtract the end of the segment from endSeg so that when we add the start of the
// next match we have actually added the length of the characters in between.
endSeg -= m.end();
}
// apply any final color to the tail segment
if (lastColor != null) {
text.addAttribute(TextAttribute.FOREGROUND, lastColor, startSeg, _text.length());
}
}
}
/**
* Computes the total width of a {@link TextLayout} given bounds returned from a call to {@link
* TextLayout#getBounds}.
*/
protected double getWidth (Rectangle2D laybounds)
{
double width = Math.max(laybounds.getX(), 0) + laybounds.getWidth();
if ((_style & OUTLINE) != 0) {
width += 2;
} else if ((_style & SHADOW) != 0) {
width += 1;
} else if ((_style & BOLD) != 0) {
width += 1;
}
return width;
}
/**
* Gets the bounds of the supplied text layout in a way that works around the various
* befuckeries that currently happen on the Mac.
*/
protected Rectangle2D getBounds (TextLayout layout)
{
if (RunAnywhere.isMacOS()) {
return layout.getOutline(null).getBounds();
} else {
return layout.getBounds();
}
}
/**
* Computes the height based on the leading, ascent and descent rather than what the layout
* reports via <code>getBounds()</code> which rarely seems to have any bearing on reality.
*/
protected float getHeight (TextLayout layout)
{
float height = layout.getLeading() + layout.getAscent() + layout.getDescent();
if ((_style & OUTLINE) != 0) {
height += 2;
} else if ((_style & SHADOW) != 0) {
height += 1;
}
return height;
}
/**
* Called when the label is changed in such a way that it must be relaid out before again being
* rendered.
*/
protected void invalidate (String where)
{
_layouts = null;
// _invalidator = where;
}
/** The text of the label. */
protected String _text;
/** The raw text, with color tags, or null if there are no color tags. */
protected String _rawText;
/** The text style. */
protected int _style;
/** The text alignment. */
protected int _align = -1; // -1 means default according to locale
/** Our size constraints in either the x or y direction. */
protected Dimension _constraints = new Dimension();
/** Our calculated size. */
protected Dimension _size = new Dimension();
/** Some fonts (God bless 'em) extend to the left of the position at which you request that
* they be rendered. We opt to push such lines to the right sufficiently that they line up with
* the rest of the lines (perhaps not the typographically ideal thing to do, but we're in
* computer land and when we say our bounds are (0, 0, width, height) we damned well better not
* render outside those bounds, which these wonderful fonts are choosing to do). */
protected float[] _leaders;
/** The font we use when laying out and rendering out text, or null if we're to use the default
* font. */
protected Font _font;
/** Formatted text layout instances that contain each line of text. */
protected TextLayout[] _layouts;
/** Formatted text layout instances that contain each line of text. */
protected Rectangle2D[] _lbounds;
/** The color in which to render the text outline or shadow if we're rendering in outline or
* shadow mode. */
protected Color _alternateColor = null;
/** The color in which to render the text or null if the text should be rendered with the
* graphics context color. */
protected Color _textColor = null;
/** Will be true only when we're drawing a textlayout for the "main" portion of the label. If
* we are in OUTLINE mode, we draw each layout 9 times: the last one is the only main one. */
protected boolean _mainDraw = true;
// /** Used for debugging. */
// protected String _invalidator;
/** An approximation of the golden ratio. */
protected static final double GOLDEN_RATIO = 1.618034;
/** Used by {@link #unescapeColors}. */
protected static final Pattern ESCAPED_PATTERN = Pattern.compile("#''([Xx]|[0-9A-Fa-f]{6}+)");
}

Some files were not shown because too many files have changed in this diff Show More