Migrated samskivert into its own, sensibly organized, repository.

git-svn-id: https://samskivert.googlecode.com/svn/trunk@1774 6335cc39-0255-0410-8fd6-9bcaacd3b74c
This commit is contained in:
mdb
2006-01-30 19:52:55 +00:00
parent 5966766e26
commit 9d62b407e7
413 changed files with 0 additions and 9443 deletions
+71
View File
@@ -0,0 +1,71 @@
//
// $Id: Log.java,v 1.3 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
package com.samskivert;
/**
* A placeholder class that contains a reference to the log object used by
* the samskivert package. This is a useful pattern to use when using the
* samskivert logging facilities. One creates a top-level class like this
* one that instantiates a log object with an name that identifies log
* messages from that package and then provides static methods that
* generate log messages using that instance. Then, classes in that
* package need only import the log wrapper class and can easily use it to
* generate log messages. For example:
*
* <pre>
* import com.samskivert.Log;
* // ...
* Log.warning("All hell is breaking loose!");
* // ...
* </pre>
*
* @see com.samskivert.util.Log
*/
public class Log
{
/** The static log instance configured for use by this package. */
public static com.samskivert.util.Log log =
new com.samskivert.util.Log("samskivert");
/** Convenience function. */
public static void debug (String message)
{
log.debug(message);
}
/** Convenience function. */
public static void info (String message)
{
log.info(message);
}
/** Convenience function. */
public static void warning (String message)
{
log.warning(message);
}
/** Convenience function. */
public static void logStackTrace (Throwable t)
{
log.logStackTrace(com.samskivert.util.Log.WARNING, t);
}
}
@@ -0,0 +1,65 @@
//
// $Id: ByteArrayOutInputStream.java,v 1.1 2002/02/01 18:19:43 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
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: ExtensiblePrintStream.java,v 1.2 2002/10/16 19:12:53 shaper Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2002 Walter Korman
//
// 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 ();
// documentation inherited
public void print (boolean b)
{
super.print(b);
handlePrinted(b ? "true" : "false");
}
// documentation inherited
public void print (char c)
{
super.print(c);
handlePrinted(String.valueOf(c));
}
// documentation inherited
public void print (int i)
{
super.print(i);
handlePrinted(String.valueOf(i));
}
// documentation inherited
public void print (long l)
{
super.print(l);
handlePrinted(String.valueOf(l));
}
// documentation inherited
public void print (float f)
{
super.print(f);
handlePrinted(String.valueOf(f));
}
// documentation inherited
public void print (double d)
{
super.print(d);
handlePrinted(String.valueOf(d));
}
// documentation inherited
public void print (char[] s)
{
super.print(s);
handlePrinted(String.valueOf(s));
}
// documentation inherited
public void print (String s)
{
super.print(s);
handlePrinted((s == null) ? "null" : s);
}
// documentation inherited
public void print (Object obj)
{
super.print(obj);
handlePrinted(String.valueOf(obj));
}
// documentation inherited
public void println ()
{
super.println();
handleNewLine();
}
// documentation inherited
public void println (boolean x)
{
super.println(x);
handleNewLine();
}
// documentation inherited
public void println (char x)
{
super.println(x);
handleNewLine();
}
// documentation inherited
public void println (int x)
{
super.println(x);
handleNewLine();
}
// documentation inherited
public void println (long x)
{
super.println(x);
handleNewLine();
}
// documentation inherited
public void println (float x)
{
super.println(x);
handleNewLine();
}
// documentation inherited
public void println (double x)
{
super.println(x);
handleNewLine();
}
// documentation inherited
public void println (char[] x)
{
super.println(x);
handleNewLine();
}
// documentation inherited
public void println (String x)
{
super.println(x);
handleNewLine();
}
// documentation inherited
public void println (Object x)
{
super.println(x);
handleNewLine();
}
}
@@ -0,0 +1,59 @@
//
// $Id: PersistenceException.java,v 1.4 2003/03/17 23:01:37 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
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,46 @@
//
// $Id: StreamUtil.java,v 1.1 2003/05/14 21:30:29 ray Exp $
package com.samskivert.io;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import com.samskivert.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 + "].");
}
}
}
}
+32
View File
@@ -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,86 @@
//
// $Id: ConnectionProvider.java,v 1.2 2001/09/21 03:01: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
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.
*
* @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)
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 conn the connection to release (back into the pool or to be
* closed if pooling is not going on).
*/
public void releaseConnection (String ident, 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 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, Connection conn,
SQLException error);
}
@@ -0,0 +1,75 @@
//
// $Id: DatabaseLiaison.java,v 1.2 2001/09/20 02:09:09 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
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);
/**
* Returns the value of an <code>AUTO_INCREMENT</code> column for the
* last row insertion. This is MySQL specific, but exists here for now
* until we sort out a general purpose mechanism for assigning unique
* ids.
*
* @return the most recent ID generated by an insert into an
* <code>AUTO_INCREMENT</code> table or -1 if it could not be
* obtained.
*/
public int lastInsertedId (Connection conn) throws SQLException;
}
@@ -0,0 +1,56 @@
//
// $Id: DefaultLiaison.java,v 1.1 2001/09/20 02:09:09 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
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 implements DatabaseLiaison
{
// documentation inherited
public boolean matchesURL (String url)
{
return true;
}
// documentation inherited
public boolean isDuplicateRowException (SQLException sqe)
{
return false;
}
// documentation inherited
public boolean isTransientException (SQLException sqe)
{
return false;
}
// documentation inherited
public int lastInsertedId (Connection conn) throws SQLException
{
return -1;
}
}
+494
View File
@@ -0,0 +1,494 @@
//
// $Id: JDBCUtil.java,v 1.10 2004/05/28 01:54:47 eric 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
package com.samskivert.jdbc;
import java.io.UnsupportedEncodingException;
import java.sql.*;
import com.samskivert.Log;
import com.samskivert.io.PersistenceException;
import com.samskivert.util.StringUtil;
/**
* A repository for JDBC related utility functions.
*/
public class JDBCUtil
{
/**
* 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();
}
}
/**
* 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 + "]");
}
}
/**
* 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 = StringUtil.replace(text, "\\", "\\\\");
return "'" + StringUtil.replace(text, "'", "\\'") + "'";
}
/**
* Escapes a list of values, separating the escaped values by
* commas. See {@link #escape(String)}.
*/
public static String escape (Object[] values)
{
StringBuffer buf = new StringBuffer();
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-8895-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.logStackTrace(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.logStackTrace(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 StringUtil.replace(jigger(text), "'", "\\'");
}
/**
* 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
{
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 (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
{
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");
int type = rs.getInt("DATA_TYPE");
if (tname.equals(table) && cname.equals(column)) {
return type;
}
}
throw new SQLException("Table or Column not defined. [table=" + table +
", col=" + column + "].");
}
/**
* 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.
*/
public static void addColumn (Connection conn, String table,
String cname, String cdef, String afterCname)
throws SQLException
{
if (JDBCUtil.tableContainsColumn(conn, table, cname)) {
// Log.info("Database table '" + table + "' already has column '" +
// cname + "'.");
return;
}
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 +
"'.");
}
/**
* 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.
*/
public static void dropColumn (Connection conn, String table, String cname)
throws SQLException
{
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);
}
}
/**
* Removes a named index from the specified table.
*/
public static void dropIndex (Connection conn, String table, String iname)
throws SQLException
{
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);
}
}
/**
* 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.
*/
public static void addIndexToTable (Connection conn, String table,
String cname, String iname)
throws SQLException
{
if (JDBCUtil.tableContainsIndex(conn, table, cname, iname)) {
// Log.info("Database table '" + table + "' already has an index " +
// "on column '" + cname + "'" +
// (iname != null ? " named '" + iname + "'." : "."));
return;
}
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 +
"'");
}
}
@@ -0,0 +1,240 @@
//
// $Id: JORARepository.java,v 1.2 2004/02/25 13:16:05 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
package com.samskivert.jdbc;
import java.sql.*;
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.Session
*/
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);
// our parent class will have already obtained a database connection
// and therefore ended up calling getConnection() which will create our
// session, so we can make use of it straight away
// create our tables
createTables(_session);
}
/**
* Inserts the supplied object into the specified table. The table
* must be configured to store items of the supplied type.
*/
protected int insert (final Table table, final Object object)
throws PersistenceException
{
Integer iid = (Integer)execute(new Operation() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
table.insert(object);
return new Integer(liaison.lastInsertedId(conn));
}
});
return iid.intValue();
}
/**
* Updates the supplied object in the specified table. The table must
* be configured to store items of the supplied type.
*/
protected void update (final Table table, final Object object)
throws PersistenceException
{
execute(new Operation() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
table.update(object);
return null;
}
});
}
/**
* 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 Object load (final Table table, final String query)
throws PersistenceException
{
return execute(new Operation() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.select(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 Object loadByExample (final Table table, final Object example)
throws PersistenceException
{
return execute(new Operation() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.queryByExample(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 Object loadByExample (
final Table table, final FieldMask mask, final Object example)
throws PersistenceException
{
return execute(new Operation() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
return table.queryByExample(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.
*/
protected void store (final Table table, final Object object)
throws PersistenceException
{
execute(new Operation() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
if (table.update(object) == 0) {
table.insert(object);
}
return null;
}
});
}
/**
* Updates the specified field in the supplied object (which must
* correspond to the supplied table).
*/
protected void updateField (
final Table table, final Object object, String field)
throws PersistenceException
{
final FieldMask mask = table.getFieldMask();
mask.setModified(field);
execute(new Operation() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
table.update(object, mask);
return null;
}
});
}
/**
* Updates the specified fields in the supplied object (which must
* correspond to the supplied table).
*/
protected void updateFields (
final Table table, final Object object, String[] fields)
throws PersistenceException
{
final FieldMask mask = table.getFieldMask();
for (int ii = 0; ii < fields.length; ii++) {
mask.setModified(fields[ii]);
}
execute(new Operation() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
table.update(object, mask);
return null;
}
});
}
protected void delete (final Table table, final Object object)
throws PersistenceException
{
execute(new Operation() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
table.delete(object);
return null;
}
});
}
/**
* After the database session is begun, this function will be called
* to give the repository implementation the opportunity to create its
* table objects.
*
* @param session the session instance to use when creating your table
* instances.
*/
protected abstract void createTables (Session session);
protected void gotConnection (Connection conn)
{
// create or update our JORA session
if (_session == null) {
_session = new Session(conn);
} else {
_session.setConnection(conn);
}
}
protected Session _session;
}
@@ -0,0 +1,93 @@
//
// $Id: LiaisonRegistry.java,v 1.4 2001/09/20 20:41:10 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
package com.samskivert.jdbc;
import java.sql.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.HashMap;
import com.samskivert.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 database
* connection.
*/
public static DatabaseLiaison getLiaison (Connection conn)
throws SQLException
{
DatabaseMetaData dmd = conn.getMetaData();
String url = dmd.getURL();
// see if we already have a liaison mapped for this connection
DatabaseLiaison liaison = (DatabaseLiaison)_mappings.get(url);
if (liaison == null) {
// scan the list looking for a matching liaison
Iterator iter = _liaisons.iterator();
while (iter.hasNext()) {
DatabaseLiaison candidate = (DatabaseLiaison)iter.next();
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 " +
"[url=" + url + "]. Using default.");
liaison = new DefaultLiaison();
}
// map this URL to this liaison
_mappings.put(url, liaison);
}
return liaison;
}
protected static void registerLiaisonClass (Class 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 _liaisons = new ArrayList();
protected static HashMap _mappings = new HashMap();
// register our liaison classes
static {
registerLiaisonClass(MySQLLiaison.class);
}
}
@@ -0,0 +1,72 @@
//
// $Id: MySQLLiaison.java,v 1.5 2002/08/19 21:18:58 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
package com.samskivert.jdbc;
import java.sql.*;
/**
* A database liaison for the MySQL database.
*/
public class MySQLLiaison implements DatabaseLiaison
{
// documentation inherited
public boolean matchesURL (String url)
{
return url.startsWith("jdbc:mysql");
}
// documentation inherited
public boolean isDuplicateRowException (SQLException sqe)
{
String msg = sqe.getMessage();
return (msg != null && msg.indexOf("Duplicate entry") != -1);
}
// documentation inherited
public boolean isTransientException (SQLException sqe)
{
String msg = sqe.getMessage();
return (msg != null &&
(msg.indexOf("Lost connection") != -1 ||
msg.indexOf("Communication link failure") != -1 ||
msg.indexOf("Broken pipe") != -1));
}
// documentation inherited
public int lastInsertedId (Connection conn) throws SQLException
{
Statement stmt = null;
// we have to do this by hand. alas all is not roses.
try {
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select LAST_INSERT_ID()");
if (rs.next()) {
return rs.getInt(1);
} else {
return -1;
}
} finally {
JDBCUtil.close(stmt);
}
}
}
@@ -0,0 +1,119 @@
//
// $Id: Repository.java,v 1.12 2001/11/01 02:08:17 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
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
{
/**
* 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 Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException;
}
/** Our database connection provider. */
protected ConnectionProvider _provider;
}
@@ -0,0 +1,362 @@
//
// $Id: SimpleRepository.java,v 1.9 2004/05/11 05:43:15 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
package com.samskivert.jdbc;
import java.sql.*;
import com.samskivert.Log;
import com.samskivert.io.PersistenceException;
import com.samskivert.util.StringUtil;
/**
* 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.
*/
public SimpleRepository (ConnectionProvider provider, String dbident)
{
super(provider);
_dbident = dbident;
// give the repository a chance to do any schema migration before
// things get further underway
try {
execute(new Operation() {
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 + "].");
Log.logStackTrace(pe);
}
}
/**
* Executes the supplied 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 Object execute (Operation op)
throws PersistenceException
{
return execute(op, true);
}
/**
* 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.
*
* @return whatever value is returned by the invoked operation.
*/
protected Object execute (Operation op, boolean retryOnTransientFailure)
throws PersistenceException
{
Connection conn = null;
DatabaseLiaison liaison = null;
Object rv = null;
boolean supportsTransactions = false;
boolean attemptedOperation = false;
// check our pre-condition
if (_precond != null && !_precond.validate(_dbident, op)) {
Log.warning("Repository operation failed pre-condition check! " +
"[dbident=" + _dbident + ", op=" + op + "].");
Thread.dumpStack();
}
try {
// obtain our database connection and associated goodies
conn = _provider.getConnection(_dbident);
liaison = LiaisonRegistry.getLiaison(conn);
// find out if we support transactions
DatabaseMetaData dmd = conn.getMetaData();
if (dmd != null) {
supportsTransactions = dmd.supportsTransactions();
}
// turn off auto-commit
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 " +
"[origerr=" + sqe + ", rberr=" + rbe + "].");
}
}
if (conn != null) {
// let the connection provider know that the connection failed
_provider.connectionFailed(_dbident, conn, sqe);
// clear out the reference so that we don't release it later
conn = null;
}
// if this is a transient failure and we've been requested to
// retry such failures, try one more time
if (retryOnTransientFailure &&
liaison != null && liaison.isTransientException(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 + "].");
return execute(op, false);
}
String err = "Operation invocation failed";
throw new PersistenceException(err, sqe);
} 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 (conn != null && supportsTransactions && !conn.isClosed()) {
conn.rollback();
}
} catch (SQLException rbe) {
Log.warning("Unable to roll back operation " +
"[origerr=" + rte + ", rberr=" + rbe + "].");
}
throw rte;
} finally {
if (conn != null) {
// release the database connection
_provider.releaseConnection(_dbident, conn);
}
}
}
/**
* Executes the supplied update query in this repository, returning
* the number of rows modified.
*/
protected int update (final String query)
throws PersistenceException
{
Integer rv = (Integer)execute(new Operation() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
Statement stmt = null;
try {
stmt = conn.createStatement();
return new Integer(stmt.executeUpdate(query));
} finally {
JDBCUtil.close(stmt);
}
}
});
return rv.intValue();
}
/**
* 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
{
execute(new Operation() {
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;
}
});
}
/**
* 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
{
execute(new Operation() {
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();
StringBuffer buf = new StringBuffer();
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,266 @@
//
// $Id: StaticConnectionProvider.java,v 1.4 2002/03/03 03:14:15 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
package com.samskivert.jdbc;
import java.io.IOException;
import java.sql.*;
import java.util.*;
import com.samskivert.Log;
import com.samskivert.io.PersistenceException;
import com.samskivert.util.ConfigUtil;
import com.samskivert.util.PropertiesUtil;
import com.samskivert.util.StringUtil;
/**
* 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;
}
/**
* Closes all of the open database connections in preparation for
* shutting down.
*/
public void shutdown ()
{
// close all of the connections
Iterator iter = _keys.keySet().iterator();
while (iter.hasNext()) {
String key = (String)iter.next();
Mapping conmap = (Mapping)_keys.get(key);
try {
conmap.connection.close();
} catch (SQLException sqe) {
Log.warning("Error shutting down connection " +
"[key=" + key + ", err=" + sqe + "].");
}
}
// clear out our mapping tables
_keys.clear();
_idents.clear();
}
// documentation inherited
public Connection getConnection (String ident)
throws PersistenceException
{
Mapping conmap = (Mapping)_idents.get(ident);
// 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);
err = "No driver password specified [ident=" + ident + "].";
String password = requireProp(props, "password", err);
// we cache connections by username+url to avoid making more
// that one connection to a particular database server
String key = username + "@" + url;
conmap = (Mapping)_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);
_keys.put(key, conmap);
} else {
Log.debug("Reusing " + key + " for " + ident + ".");
}
// cache the connection
conmap.idents.add(ident);
_idents.put(ident, conmap);
}
return conmap.connection;
}
// documentation inherited
public void releaseConnection (String ident, Connection conn)
{
// nothing to do here, all is well
}
// documentation inherited
public void connectionFailed (String ident, Connection conn,
SQLException error)
{
Mapping conmap = (Mapping)_idents.get(ident);
if (conmap == null) {
Log.warning("Unknown connection failed!? [ident=" + ident + "].");
return;
}
// attempt to close the connection
try {
conmap.connection.close();
} catch (SQLException sqe) {
Log.warning("Error closing failed connection [ident=" + ident +
", error=" + sqe + "].");
}
// 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);
}
protected Connection openConnection (
String driver, String url, String username, String password)
throws PersistenceException
{
// load up the driver class
try {
Class.forName(driver);
} catch (Exception e) {
String err = "Error loading driver [class=" + driver + "].";
throw new PersistenceException(err, e);
}
// create the connection
try {
return DriverManager.getConnection(url, username, password);
} catch (SQLException sqe) {
String err = "Error creating database connection " +
"[driver=" + driver + ", url=" + url +
", username=" + username + "].";
throw new PersistenceException(err, sqe);
}
}
protected static String requireProp (
Properties props, String name, String errmsg)
throws PersistenceException
{
String value = props.getProperty(name);
if (StringUtil.isBlank(value)) {
// augment the error message
errmsg = "Unable to get connection. " + errmsg;
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 ArrayList idents = new ArrayList();
}
/** Our configuration in the form of a properties object. */
protected Properties _props;
/** A mapping from database identifier to connection records. */
protected HashMap _idents = new HashMap();
/** A mapping from connection key to connection records. */
protected HashMap _keys = new HashMap();
/** The key used as defaults for the database definitions. */
protected static final String DEFAULTS_KEY = "default";
}
@@ -0,0 +1,288 @@
//-< Cursor.java >---------------------------------------------------*--------*
// JORA Version 2.0 (c) 1998 GARRET * ? *
// (Java Object Relational Adapter) * /\| *
// * / \ *
// Created: 10-Jun-98 K.A. Knizhnik * / [] \ *
// Last update: 20-Jun-98 K.A. Knizhnik * GARRET *
//-------------------------------------------------------------------*--------*
// Cursor for navigation thru result of SELECT statement
//-------------------------------------------------------------------*--------*
package com.samskivert.jdbc.jora;
import java.util.*;
import java.sql.*;
import com.samskivert.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 {
/**
* 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 Object next()
throws SQLException
{
// if we closed everything up after the last call to next(),
// nTables will be zero here and we should bail immediately
if (nTables == 0) {
return null;
}
// try {
do {
if (result == null) {
if (table.isAbstract) {
table = table.derived;
continue;
}
if (qbeObject != null) {
PreparedStatement qbeStmt;
synchronized(session.preparedStmtHash) {
Object s = session.preparedStmtHash.get(query);
if (s == null) {
qbeStmt=
session.connection.prepareStatement(query);
session.preparedStmtHash.put(query, qbeStmt);
} else {
qbeStmt = (PreparedStatement)s;
}
}
synchronized(qbeStmt) {
table.bindQueryVariables(
qbeStmt, qbeObject, qbeMask);
result = qbeStmt.executeQuery();
qbeStmt.clearParameters();
}
} else {
if (stmt == null) {
stmt = session.connection.createStatement();
}
result = stmt.executeQuery(query);
}
}
if (result.next()) {
return currObject = table.load(result);
}
result.close();
result = null;
currObject = null;
table = table.derived;
} while (--nTables != 0);
if (stmt != null) {
stmt.close();
}
// }
// catch (SQLException ex) { session.handleSQLException(ex); }
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 Object get()
throws SQLException
{
Object 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 NoCurrentObjectError();
}
// try {
table.updateVariables(result, currObject);
// }
// catch (SQLException ex) { session.handleSQLException(ex); }
}
/** 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 NoCurrentObjectError();
}
// try {
result.deleteRow();
// }
// catch (SQLException ex) { session.handleSQLException(ex); }
}
/**
* Close the Cursor, even if we haven't read all the possible
* objects.
*/
public void close ()
throws SQLException
{
if (stmt != null) {
stmt.close(); // also automatically closes result
result = null;
stmt = null;
}
nTables = 0;
}
/** 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 Array with objects constructed from fetched records.
*/
public Object[] toArray(int maxElements)
throws SQLException
{
Vector v = new Vector(maxElements < 100 ? maxElements : 100);
Object o;
while (--maxElements >= 0 && (o = next()) != null) {
v.addElement(o);
}
Object[] a = new Object[v.size()];
v.copyInto(a);
return a;
}
/** Store all objects returned by SELECT query into array 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 Object[] toArray()
throws SQLException
{ return toArray(Integer.MAX_VALUE); }
/** 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 List toArrayList(int maxElements)
throws SQLException
{
ArrayList al = new ArrayList(maxElements < 100 ? maxElements : 100);
Object 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 List toArrayList()
throws SQLException
{ return toArrayList(Integer.MAX_VALUE); }
// Internals
protected Cursor(Table table, Session session, int nTables, String query) {
if (session == null) {
session = ((SessionThread)Thread.currentThread()).session;
}
this.table = table;
this.session = session;
this.nTables = nTables;
this.query = query;
}
protected Cursor(Table table, Session session, int nTables,
Object obj, FieldMask mask, boolean like) {
if (session == null) {
session = ((SessionThread)Thread.currentThread()).session;
}
this.table = table;
this.session = session;
this.nTables = nTables;
this.like = like;
qbeObject = obj;
qbeMask = mask;
query = table.buildQueryList(obj, mask, like);
stmt = null;
}
private Table table;
private Session session;
private int nTables;
private ResultSet result;
private String query;
private Statement stmt;
private Object currObject;
private Object qbeObject;
private FieldMask qbeMask;
private boolean like;
}
@@ -0,0 +1,26 @@
//-< DataTransferError.java >----------------------------------------*--------*
// JORA Version 2.0 (c) 1998 GARRET * ? *
// (Java Object Relational Adapter) * /\| *
// * / \ *
// Created: 10-Jun-98 K.A. Knizhnik * / [] \ *
// Last update: 19-Jun-98 K.A. Knizhnik * GARRET *
//-------------------------------------------------------------------*--------*
// Exception raised when error is happed during data transfer between
// program and database server
//-------------------------------------------------------------------*--------*
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.getMessage());
}
}
@@ -0,0 +1,430 @@
//-< FieldDescriptor.java >------------------------------------------*--------*
// JORA Version 2.0 (c) 1998 GARRET * ? *
// (Java Object Relational Adapter) * /\| *
// * / \ *
// Created: 10-Jun-98 K.A. Knizhnik * / [] \ *
// Last update: 25-Jun-98 K.A. Knizhnik * GARRET *
//-------------------------------------------------------------------*--------*
// Table field descriptor
//-------------------------------------------------------------------*--------*
package com.samskivert.jdbc.jora;
import java.sql.*;
import java.math.*;
import java.lang.reflect.*;
class FieldDescriptor {
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
};
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 : new Byte(b));
break;
case tShort:
short s = result.getShort(column);
field.set(obj, result.wasNull() ? null : new Short(s));
break;
case tInteger:
int i = result.getInt(column);
field.set(obj, result.wasNull() ? null : new Integer(i));
break;
case tLong:
long l = result.getLong(column);
field.set(obj, result.wasNull() ? null : new Long(l));
break;
case tFloat:
float f = result.getFloat(column);
field.set(obj, result.wasNull() ? null : new Float(f));
field.setFloat(obj, result.getFloat(column));
break;
case tDouble:
double d = result.getDouble(column);
field.set(obj, result.wasNull() ? null : new Double(d));
break;
case tBoolean:
boolean bl = result.getBoolean(column);
field.set(obj, result.wasNull() ? null : new Boolean(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;
}
}
@@ -0,0 +1,180 @@
//
// $Id: FieldMask.java,v 1.6 2003/09/20 00:04:34 ray 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
package com.samskivert.jdbc.jora;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
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)
{
// create a mapping from field name to descriptor index
_descripMap = new HashMap();
int dcount = descrips.length;
for (int i = 0; i < dcount; i++) {
_descripMap.put(descrips[i].field.getName(), new Integer(i));
}
// create our modified flags
_modified = new boolean[dcount];
}
/**
* 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 = (Integer)_descripMap.get(fieldName);
if (index == null) {
String errmsg = "Passed in field not in mask.";
throw new IllegalArgumentException(errmsg);
}
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 fieldSet)
{
Iterator itr = _descripMap.keySet().iterator();
while (itr.hasNext()) {
String field = (String)itr.next();
if (isModified(field) && (!fieldSet.contains(field))) {
return false;
}
}
return true;
}
/**
* Marks the specified field as modified.
*/
public void setModified (String fieldName)
{
Integer index = (Integer)_descripMap.get(fieldName);
if (index == null) {
String errmsg = "";
throw new IllegalArgumentException(errmsg);
}
_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.
*/
public Object clone ()
{
try {
FieldMask mask = (FieldMask)super.clone();
mask._modified = new boolean[_modified.length];
return mask;
} catch (CloneNotSupportedException cnse) {
throw new RuntimeException("Oh god, the clones!");
}
}
// documentation inherited
public String toString ()
{
// return a list of the modified fields
StringBuffer buf = new StringBuffer("FieldMask [modified={");
boolean added = false;
for (Iterator itr=_descripMap.entrySet().iterator(); itr.hasNext(); ) {
Map.Entry entry = (Map.Entry) itr.next();
if (_modified[((Integer) entry.getValue()).intValue()]) {
if (added) {
buf.append(", ");
} else {
added = true;
}
buf.append(entry.getKey());
}
}
buf.append("}]");
return buf.toString();
}
/** Modified flags for each field of an object in this table. */
protected boolean[] _modified;
/** A mapping from field names to field descriptor index. */
protected HashMap _descripMap;
}
@@ -0,0 +1,23 @@
//-< NoCurrentObjectError.java >-------------------------------------*--------*
// JORA Version 2.0 (c) 1998 GARRET * ? *
// (Java Object Relational Adapter) * /\| *
// * / \ *
// Created: 10-Jun-98 K.A. Knizhnik * / [] \ *
// Last update: 16-Jun-98 K.A. Knizhnik * GARRET *
//-------------------------------------------------------------------*--------*
// Exception raised when UPDATE/REMOVE operation is applied to Cursor
// with no current object
//-------------------------------------------------------------------*--------*
package com.samskivert.jdbc.jora;
/** Error raised when update/remove operation is applied to
* cursor with no current object (before first call of <TT>next()</TT>
* method).
*/
public class NoCurrentObjectError extends java.lang.Error {
NoCurrentObjectError() {
super("Cursor doesn't specify current object");
}
}
@@ -0,0 +1,22 @@
//-< NoPrimaryKeyError.java >----------------------------------------*--------*
// JORA Version 2.0 (c) 1998 GARRET * ? *
// (Java Object Relational Adapter) * /\| *
// * / \ *
// Created: 10-Jun-98 K.A. Knizhnik * / [] \ *
// Last update: 16-Jun-98 K.A. Knizhnik * GARRET *
//-------------------------------------------------------------------*--------*
// Exception raised when UPDATE/REMOVE operation is appllied to Table with
// no primary key
//-------------------------------------------------------------------*--------*
package com.samskivert.jdbc.jora;
/** Error raised when unpdate/remove operation is invoked for the table
* with no correct primary key defined (key was not specified or
* type of the key component is not atomic).
*/
public class NoPrimaryKeyError extends java.lang.Error {
NoPrimaryKeyError(Table table) {
super("Table " + table.name + " has no atomic primary key");
}
}
+29
View File
@@ -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,21 @@
//-< SQLError.java >-------------------------------------------------*--------*
// JORA Version 2.0 (c) 1998 GARRET * ? *
// (Java Object Relational Adapter) * /\| *
// * / \ *
// Created: 10-Jun-98 K.A. Knizhnik * / [] \ *
// Last update: 16-Jun-98 K.A. Knizhnik * GARRET *
//-------------------------------------------------------------------*--------*
// Database error
//-------------------------------------------------------------------*--------*
package com.samskivert.jdbc.jora;
/** Database error. Exception SQLException was catched by JORA.
*/
public class SQLError extends java.lang.RuntimeException {
java.sql.SQLException ex;
SQLError(java.sql.SQLException x) {
super("Database session aborted due to critical error");
ex = x;
}
}
@@ -0,0 +1,203 @@
//-< Session.java >--------------------------------------------------*--------*
// JORA Version 2.0 (c) 1998 GARRET * ? *
// (Java Object Relational Adapter) * /\| *
// * / \ *
// Created: 10-Jun-98 K.A. Knizhnik * / [] \ *
// Last update: 20-Jun-98 K.A. Knizhnik * GARRET *
//-------------------------------------------------------------------*--------*
// Database session abstraction
//-------------------------------------------------------------------*--------*
package com.samskivert.jdbc.jora;
import java.util.*;
import java.sql.*;
import com.samskivert.Log;
/**
* This class is reposnsible for establishing connection with database
* and handling database errors.
*/
public class Session {
public Connection connection;
/** Session consructor
*
* @param driverClass class of database driver
*/
public Session(String driverClass)
{
driver = driverClass;
preparedStmtHash = new Hashtable();
connectionID = 0;
}
/** Construct a session with a pre-existing connection instance. In
* this case, {@link #open} should not be called on this session.
*
* @param connection the connection to use.
*/
public Session(Connection connection)
{
connectionID = 0;
preparedStmtHash = new Hashtable();
setConnection(connection);
}
/** Session consructor for ODBC bridge driver
*/
public Session() { this("sun.jdbc.odbc.JdbcOdbcDriver"); }
/** Sets the connection that should be used by this session.
*/
public void setConnection(Connection conn)
{
// only up the connection id if this is a new connection
if (connection != conn) {
// clear out our prepared statement hash because we've got a
// new connection
Enumeration items = preparedStmtHash.elements();
while (items.hasMoreElements()) {
try {
((PreparedStatement)items.nextElement()).close();
} catch (SQLException sqe) {
Log.warning("Error closing cached prepared statement " +
"[error=" + sqe + "].");
}
}
preparedStmtHash.clear();
// switch to our new connection
connection = conn;
connectionID += 1;
}
}
/** Handler of database session errors. Programmer should override
* this method in derived class in order to provide application
* dependent error handling.
*
* @param ex exception thrown by some of JDBC methods
*/
public void handleSQLException(SQLException ex) {
// A SQLException was generated. Catch it and
// display the error information. Note that there
// could be multiple error objects chained
// together
System.out.println ("*** SQLException caught ***");
SQLException x = ex;
while (ex != null) {
System.out.println("SQLState: " + ex.getSQLState ());
System.out.println("Message: " + ex.getMessage ());
System.out.println("Vendor: " + ex.getErrorCode ());
ex = ex.getNextException();
System.out.println ("");
}
throw new SQLError(x); // terminate program execution
}
/** Open database session.
* Attempt to establish a connection to the given database URL.
* The DriverManager attempts to select an appropriate driver from
* the set of registered JDBC drivers.
*
* @param dataSource a database url of the form
* jdbc:<em>subprotocol</em>:<em>subname</em>
* @param user the database user on whose behalf the Connection is
* being made
* @param password the user's password
* @return true if session is succesfully openned, false otherwise
*/
public boolean open(String dataSource, String user, String password)
throws SQLException
{
try {
Class.forName(driver);
connection =
DriverManager.getConnection(dataSource, user, password);
connectionID += 1;
}
// catch(SQLException ex) {
// handleSQLException(ex);
// return false;
// }
catch(ClassNotFoundException ex) {
return false;
}
return true;
}
/** Close database session and release all resources holded by session.
*/
public void close()
throws SQLException
{
// try {
Enumeration items = preparedStmtHash.elements();
while (items.hasMoreElements()) {
((PreparedStatement)items.nextElement()).close();
}
preparedStmtHash.clear();
if (connection != null) {
connection.close();
}
// }
// catch (SQLException ex) { handleSQLException(ex); }
}
/**
* Execute a SQL INSERT, UPDATE or DELETE statement. In addition,
* SQL statements that return nothing such as SQL DDL statements
* can be executed.
*
* @param sql a SQL INSERT, UPDATE or DELETE statement or a SQL
* statement that returns nothing
* @return either the row count for INSERT, UPDATE or DELETE or 0
* for SQL statements that return nothing
*/
public int execute(String sql)
throws SQLException
{
// try {
Statement stmt = connection.createStatement();
int result = stmt.executeUpdate(sql);
stmt.close();
return result;
// } catch(SQLException ex) { handleSQLException(ex); }
// return -1;
}
/**
* Commit makes all changes made since the previous
* commit/rollback permanent and releases any database locks
* currently held by the Connection. This method should only be
* used when auto commit has been disabled.
*/
public void commit()
throws SQLException
{
// try {
connection.commit();
// } catch(SQLException ex) { handleSQLException(ex); }
}
/**
* Rollback drops all changes made since the previous
* commit/rollback and releases any database locks currently held
* by the Connection. This method should only be used when auto
* commit has been disabled.
*/
public void rollback()
throws SQLException
{
// try {
connection.rollback();
// } catch(SQLException ex) { handleSQLException(ex); }
}
protected String driver; // driver class name
protected Hashtable preparedStmtHash;
protected int connectionID;
}
@@ -0,0 +1,148 @@
//-< SessionThread.java >--------------------------------------------*--------*
// JORA Version 2.0 (c) 1998 GARRET * ? *
// (Java Object Relational Adapter) * /\| *
// * / \ *
// Created: 18-Jun-99 K.A. Knizhnik * / [] \ *
// Last update: 18-Jun-99 K.A. Knizhnik * GARRET *
//-------------------------------------------------------------------*--------*
// Thread associated with database session
//-------------------------------------------------------------------*--------*
package com.samskivert.jdbc.jora;
/**
* Class representing thread associated with users database session.
* If there is single database session opened by application, that it is
* possible to associate it with Table object statically. Otherwise it is
* needed either to explicitly specify session object in each insert,select
* or update statement or associate session with thread by means of
* SessionThread class.
*/
public class SessionThread extends Thread {
Session session;
/**
* Allocates a new <code>SessionThread</code> object and associate it with
* the specified session. This constructor has the same effect as
* <code>SessionThread(session, null, null,</code>
* <i>gname</i><code>)</code>, where <b><i>gname</i></b> is
* a newly generated name. Automatically generated names are of the
* form <code>"Thread-"+</code><i>n</i>, where <i>n</i> is an integer.
* <p>
* Threads created this way must have overridden their
* <code>run()</code> method to actually do anything.
*
* @param session user database session associated with this thread
* @see java.lang.Thread#Thread(java.lang.ThreadGroup,
* java.lang.Runnable, java.lang.String)
*/
public SessionThread(Session session) {
this.session = session;
}
/**
* Allocates a new <code>SessionThread</code> object and associate it with
* the specified session. This constructor has the same effect as
* <code>SessionThread(session, target, null,</code>
* <i>gname</i><code>)</code>, where <b><i>gname</i></b> is
* a newly generated name. Automatically generated names are of the
* form <code>"Thread-"+</code><i>n</i>, where <i>n</i> is an integer.
* <p>
* Threads created this way must have overridden their
* <code>run()</code> method to actually do anything.
*
* @param session user database session associated with this thread
* @param target the object whose <code>run</code> method is called.
* @see java.lang.Thread#Thread(java.lang.ThreadGroup,
* java.lang.Runnable, java.lang.String)
*/
public SessionThread(Session session, Runnable target) {
super(target);
this.session = session;
}
/**
* Allocates a new <code>SessionThread</code> object and associate it with
* the specified session. This constructor has the same effect as
* <code>SessionThread(session, target, group,</code>
* <i>gname</i><code>)</code>, where <b><i>gname</i></b> is
* a newly generated name. Automatically generated names are of the
* form <code>"Thread-"+</code><i>n</i>, where <i>n</i> is an integer.
* <p>
* Threads created this way must have overridden their
* <code>run()</code> method to actually do anything.
*
* @param session user database session associated with this thread
* @param group the thread group.
* @param target the object whose <code>run</code> method is called.
* @see java.lang.Thread#Thread(java.lang.ThreadGroup,
* java.lang.Runnable, java.lang.String)
*/
public SessionThread(Session session, ThreadGroup group, Runnable target) {
super(target);
this.session = session;
}
/**
* Allocates a new <code>SessionThread</code> object. This constructor has
* the same effect as <code>SessionThread(session, null, null, name)</code>.
*
* @param session user database session associated with this thread
* @param name the name of the new thread.
* @see java.lang.Thread#Thread(java.lang.ThreadGroup,
* java.lang.Runnable, java.lang.String)
*/
public SessionThread(Session session, String name) {
super(name);
this.session = session;
}
/**
* Allocates a new <code>SessionThread</code> object. This constructor has
* the same effect as <code>SessionThread(session, group, null, name)</code>.
*
* @param session user database session associated with this thread
* @param group the thread group.
* @param name the name of the new thread.
* @see java.lang.Thread#Thread(java.lang.ThreadGroup,
* java.lang.Runnable, java.lang.String)
*/
public SessionThread(Session session, ThreadGroup group, String name) {
super(group, name);
this.session = session;
}
/**
* Allocates a new <code>SessionThread</code> object. This constructor has
* the same effect as <code>SessionThread(session, null, target, name)</code> *
* @param session user database session associated with this thread
* @param target the object whose <code>run</code> method is called.
* @param name the name of the new thread.
* @see java.lang.Thread#Thread(java.lang.ThreadGroup,
* java.lang.Runnable, java.lang.String)
*/
public SessionThread(Session session, Runnable target, String name) {
super(target, name);
this.session = session;
}
/**
* Allocates a new <code>SessionThread</code> object so that it has
* <code>target</code> as its run object, has the specified
* <code>name</code> as its name, and belongs to the thread group
* referred to by <code>group</code>.
* <p>
* @param session user database session associated with this thread
* @param group the thread group.
* @param target the object whose <code>run</code> method is called.
* @param name the name of the new thread.
* @see java.lang.Thread#Thread(java.lang.ThreadGroup,
* java.lang.Runnable, java.lang.String)
*/
public SessionThread(Session session, ThreadGroup group, Runnable target,
String name)
{
super(group, target, name);
this.session = session;
}
}
File diff suppressed because it is too large Load Diff
@@ -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>
+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">
Provides support services to applications that access relational
databases via JDBC.
</body>
</html>
@@ -0,0 +1,91 @@
//
// $Id: AttachableURLFactory.java,v 1.1 2003/07/09 18:44:11 ray Exp $
package com.samskivert.net;
import java.net.URL;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.HashMap;
import com.samskivert.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 handlerClass)
{
if (!URLStreamHandler.class.isAssignableFrom(handlerClass)) {
throw new IllegalArgumentException(
"Specified class is not a java.net.URLStreamHandler.");
}
// set up the factory.
if (_handlers == null) {
_handlers = new HashMap();
// 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 handler = (Class) _handlers.get(protocol.toLowerCase());
if (handler != null) {
try {
return (URLStreamHandler) 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 _handlers;
}
@@ -0,0 +1,80 @@
//
// $Id: HttpPostUtil.java,v 1.2 2003/10/08 23:52:49 ray Exp $
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 waiter = new ServiceWaiter(
(timeout < 0) ? ServiceWaiter.NO_TIMEOUT : timeout);
Thread tt = new Thread() {
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()));
StringBuffer buf = new StringBuffer();
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 (String) waiter.getArgument();
} else {
throw (IOException) waiter.getArgument();
}
}
}
+161
View File
@@ -0,0 +1,161 @@
//
// $Id: MACUtil.java,v 1.12 2004/07/02 19:53:26 eric Exp $
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;
import com.samskivert.util.StringUtil;
/**
* 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 list = new ArrayList();
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 (String[])list.toArray(new String[0]);
}
/**
* Takes a lists of commands and trys to run each one till one works.
* The idea being to beable 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 specificed 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()));
StringBuffer buffer= new StringBuffer();
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 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 String[] WINDOWS_CMDS = {"ipconfig /all"};
protected static String[] UNIX_CMDS = {"/sbin/ifconfig", "/etc/ifconfig"};
}
+142
View File
@@ -0,0 +1,142 @@
//
// $Id: MailUtil.java,v 1.9 2004/01/15 21:41:00 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
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.PatternSyntaxException;
import java.util.regex.Pattern;
import com.samskivert.Log;
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();
}
/**
* 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 {
Properties props = System.getProperties();
if (props.getProperty("mail.smtp.host") == null) {
props.put("mail.smtp.host", "localhost");
}
Session session = Session.getDefaultInstance(props, null);
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(sender));
for (int ii = 0; ii < recipients.length; ii++) {
message.addRecipient(Message.RecipientType.TO,
new InternetAddress(recipients[ii]));
}
int hcount = (headers == null) ? 0 : headers.length;
for (int ii = 0; ii < hcount; ii++) {
message.addHeader(headers[ii], values[ii]);
}
message.setSubject(subject);
message.setText(body);
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;
}
}
/** 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 Pattern _emailre;
static {
try {
_emailre = Pattern.compile(EMAIL_REGEX);
} catch (PatternSyntaxException pse) {
Log.warning("Unable to initialize email regexp?!");
Log.logStackTrace(pse);
}
}
}
+71
View File
@@ -0,0 +1,71 @@
//
// $Id: PathUtil.java,v 1.3 2001/08/13 23:24:24 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
package com.samskivert.net;
/**
* 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;
}
}
}
+516
View File
@@ -0,0 +1,516 @@
//
// $Id: CDDB.java,v 1.8 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
package com.samskivert.net.cddb;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.StringTokenizer;
/**
* 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 String CLIENT_VERSION; // assigned during static init
/**
* 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
StringBuffer req = new StringBuffer("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 <code>connect()</code>. If the connection was not
* previously established, this member function does nothing.
*
* @see #connect
*/
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
StringBuffer req = new StringBuffer("cddb query ");
req.append(discid).append(" ");
req.append(frameOffsets.length).append(" ");
for (int i = 0; i < frameOffsets.length; i++) {
req.append(frameOffsets[i]).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 list = new ArrayList();
String input = _in.readLine();
System.out.println("...: " + input);
while (!input.equals(CDDBProtocol.TERMINATOR)) {
Entry e = new Entry();
e.parse(input);
list.add(e);
input = _in.readLine();
System.out.println("...: " + input);
}
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
StringBuffer req = new StringBuffer("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 tnames = new ArrayList();
ArrayList texts = new ArrayList();
// 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 list, int index, String value)
{
// expand the list as necessary
while (list.size() <= index) {
list.add("");
}
list.set(index, list.get(index) + value);
}
/**
* A simple class to encapsulate the response from the CDDB server.
*/
protected class Response
{
public int code;
public String message;
}
/**
* 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;
}
/**
* The client version number is extracted from the version control
* revision of this file from a string that is managed by the version
* control system.
*/
static
{
StringTokenizer tok = new StringTokenizer("$Revision: 1.8 $");
tok.nextToken();
CLIENT_VERSION = tok.nextToken();
}
protected Socket _sock;
protected BufferedReader _in;
protected PrintStream _out;
}
@@ -0,0 +1,45 @@
//
// $Id: CDDBException.java,v 1.2 2001/08/11 22:43:28 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
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: CDDBProtocol.java,v 1.2 2001/08/11 22:43:28 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
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,96 @@
//
// $Id: CDDBTest.java,v 1.3 2001/08/11 22:43:28 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
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>
+32
View File
@@ -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>
+74
View File
@@ -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,64 @@
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2004 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.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,71 @@
//
// $Id: IndiscriminateSiteIdentifier.java,v 1.3 2003/11/13 00:53:00 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
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 enumerateSites ()
{
return _sites.iterator();
}
protected static ArrayList _sites = new ArrayList();
static {
_sites.add(new Site(DEFAULT_SITE_ID, DEFAULT_SITE_STRING));
}
}
@@ -0,0 +1,337 @@
//
// $Id: JDBCTableSiteIdentifier.java,v 1.12 2004/02/25 13:17:13 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
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.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 com.samskivert.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
{
_repo = new SiteIdentifierRepository(conprov);
// load up our site data
_repo.refreshSiteData();
}
// 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 = (SiteMapping)_mappings.get(i);
if (serverName.endsWith(mapping.domain)) {
return mapping.siteId;
}
}
// if we matched nothing, return the default id
return DEFAULT_SITE_ID;
}
// documentation inherited
public String getSiteString (int siteId)
{
checkReloadSites();
Site site = (Site)_sitesById.get(siteId);
return (site == null) ? DEFAULT_SITE_STRING : site.siteString;
}
// documentation inherited
public int getSiteId (String siteString)
{
checkReloadSites();
Site site = (Site)_sitesByString.get(siteString);
return (site == null) ? DEFAULT_SITE_ID : site.siteId;
}
// documentation inherited from interface
public Iterator 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,
// avoiding causing enumerateSites() to choke
HashMap newStrings = (HashMap) _sitesByString.clone();
HashIntMap newIds = (HashIntMap) _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) {
if (reload = (now - _lastReload > RELOAD_INTERVAL)) {
_lastReload = now;
}
}
if (reload) {
try {
_repo.refreshSiteData();
} catch (PersistenceException pe) {
Log.warning("Error refreshing site data.");
Log.logStackTrace(pe);
}
}
}
/**
* Used to load information from the site database.
*/
protected class SiteIdentifierRepository extends SimpleRepository
implements SimpleRepository.Operation
{
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 sites = new HashIntMap();
HashMap strings = new HashMap();
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 mappings = new ArrayList();
while (rs.next()) {
mappings.add(new SiteMapping(rs.getInt(2),
rs.getString(1)));
}
// sort the mappings in order of specificity
Collections.sort(mappings);
_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
{
execute(new Operation() {
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);
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
}
/**
* Used to track domain to site identifier mappings.
*/
protected static class SiteMapping implements Comparable
{
/** 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);
}
/**
* Site mappings sort from most specific (www.yahoo.com) to least
* specific (yahoo.com).
*/
public int compareTo (Object other)
{
if (other instanceof SiteMapping) {
SiteMapping orec = (SiteMapping)other;
return orec._rdomain.compareTo(_rdomain);
} else {
// no comparablo
return getClass().getName().compareTo(
other.getClass().getName());
}
}
/** Returns a string representation of this site mapping. */
public String toString ()
{
return "[" + domain + " => " + siteId + "]";
}
protected String _rdomain;
}
/** The repository through which we load up site identifier
* information. */
protected SiteIdentifierRepository _repo;
/** The list of domain to site identifier mappings ordered from most
* specific domain to least specific. */
protected volatile ArrayList _mappings = new ArrayList();
/** The mapping from integer site identifiers to string site
* identifiers. */
protected volatile HashIntMap _sitesById = new HashIntMap();
/** The mapping from string site identifiers to integer site
* identifiers. */
protected volatile HashMap _sitesByString = new HashMap();
/** 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,317 @@
//
// $Id: MessageManager.java,v 1.11 2004/05/11 03:14:03 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
package com.samskivert.servlet;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.*;
import javax.servlet.http.HttpServletRequest;
import com.samskivert.Log;
import com.samskivert.text.MessageUtil;
import com.samskivert.util.StringUtil;
/**
* 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.
*
* @see java.util.ResourceBundle
*/
public MessageManager (String bundlePath, Locale deflocale)
{
// keep these for later
_bundlePath = bundlePath;
_deflocale = deflocale;
}
/**
* 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.
* @param siteIdent a site identifier that can be used to identify the
* site via which an http request was made.
*/
public void activateSiteSpecificMessages (String siteBundlePath,
SiteResourceLoader siteLoader,
SiteIdentifier siteIdent)
{
_siteBundlePath = siteBundlePath;
_siteLoader = siteLoader;
_siteIdent = siteIdent;
}
/**
* 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 strip the taint character
if (path.startsWith(MessageUtil.TAINT_CHAR)) {
return path.substring(1);
}
// 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 i = 0; i < args.length; i++) {
// if the argument is tainted, do no further translation
// (it might contain |s or other fun stuff)
if (args[i].startsWith(MessageUtil.TAINT_CHAR)) {
args[i] = MessageUtil.unescape(args[i].substring(1));
} else {
args[i] = getMessage(req, MessageUtil.unescape(args[i]));
}
}
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);
if (bundles != null) {
int blength = bundles.length;
for (int i = 0; i < blength; i++) {
try {
if (bundles[i] != null) {
return bundles[i].getString(path);
}
} catch (MissingResourceException mre) {
// no complaints, just try the bundle in the enclosing
// scope
}
}
}
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(BUNDLE_CACHE_NAME);
}
if (bundles != null) {
return bundles;
}
// grab our site-specific class loader if we have one
ClassLoader siteLoader = null;
if (_siteLoader != null && _siteIdent != null) {
int siteId = _siteIdent.identifySite(req);
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);
}
// then from the default classloader
bundles[1] = resolveBundle(req, _bundlePath,
getClass().getClassLoader());
// if we found either or both bundles, cache 'em
if (bundles[0] != null || bundles[1] != null && req != null) {
req.setAttribute(BUNDLE_CACHE_NAME, 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)
{
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 we were unable even to find a default bundle, we've
// got real problems. time to freak out
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();
}
/** 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_NAME =
"com.samskivert.servlet.MessageManager:CachedResourceBundle";
}
@@ -0,0 +1,42 @@
//
// $Id: RedirectException.java,v 1.2 2001/08/11 22:43:28 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
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();
}
}
+42
View File
@@ -0,0 +1,42 @@
//
// $Id: Site.java,v 1.2 2003/11/13 00:53:00 mdb Exp $
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 ()
{
}
/**
* Generates a string representation of this instance.
*/
public String toString ()
{
return StringUtil.fieldsToString(this);
}
}
@@ -0,0 +1,90 @@
//
// $Id: SiteIdentifier.java,v 1.3 2003/11/13 00:53:00 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
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 enumerateSites ();
}
@@ -0,0 +1,383 @@
//
// $Id: SiteResourceLoader.java,v 1.7 2004/05/11 03:14:03 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
package com.samskivert.servlet;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.servlet.http.HttpServletRequest;
import com.samskivert.Log;
import com.samskivert.util.HashIntMap;
/**
* 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 = (ClassLoader)_loaders.get(siteId);
// create one if we've not
if (loader == null) {
SiteResourceBundle bundle = getBundle(siteId);
if (bundle == null) {
// no bundle... no classloader.
return null;
}
loader = new SiteClassLoader(bundle);
_loaders.put(siteId, loader);
}
return loader;
}
}
/**
* Returns a string representation of this instance.
*/
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 = (SiteResourceBundle)
_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;
}
/**
* Returns a string representation of this instance.
*/
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;
}
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;
}
}
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 _locks = new HashIntMap();
/** The table of site-specific jar file information. */
protected HashIntMap _bundles = new HashIntMap();
/** The table of site-specific class loaders. */
protected HashIntMap _loaders = new HashIntMap();
/** 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: AuthenticationFailedException.java,v 1.1 2002/05/02 19:10:34 shaper Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2002 Walter Korman
//
// 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,49 @@
//
// $Id: Authenticator.java,v 1.1 2002/05/02 19:10:34 shaper Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2002 Walter Korman
//
// 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.
* @param persist if true, the cookie will expire in one month, if
* false, the cookie will expire in 24 hours.
*
* @throws AuthenticationFailedException if the user failed to pass
* the authentication check.
*/
public void authenticateUser (
User user, String username, Password password, boolean persist)
throws AuthenticationFailedException;
}
@@ -0,0 +1,32 @@
//
// $Id: InvalidPasswordException.java,v 1.4 2002/05/02 19:10:34 shaper 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
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: InvalidUsernameException.java,v 1.3 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
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: NoSuchUserException.java,v 1.4 2002/05/02 19:10:34 shaper 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
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,56 @@
//
// $Id: Password.java,v 1.1 2004/01/31 06:24:01 mdb Exp $
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,181 @@
//
// $Id: User.java,v 1.10 2004/01/13 23:02:04 ray 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
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 real name.
*/
public void setRealName (String realname)
{
this.realname = realname;
_dirty.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();
_dirty.setModified("password");
}
/**
* Updates the user's email address.
*/
public void setEmail (String email)
{
this.email = email;
_dirty.setModified("email");
}
/**
* Updates the user's site id.
*/
public void setSiteId (int siteId)
{
this.siteId = siteId;
_dirty.setModified("siteId");
}
/**
* Converts a legacy password.
*
* @return true if a conversion took place, false if not.
*/
public boolean updateLegacyPassword (String password)
{
// we may have an old crypt() encrypted password
if (this.password.length() == 13) {
// check both the case sensitive and insensitive versions for
// further legacy reasons
if (this.password.equals(
UserUtil.legacyEncrypt(username, password, false)) ||
this.password.equals(
UserUtil.legacyEncrypt(username, password, true))) {
// update our password with the new encryption method
setPassword(password);
return true;
}
}
return false;
}
/**
* 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('='));
}
/**
* 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;
}
/** Returns a string representation of this instance. */
public String toString ()
{
return StringUtil.fieldsToString(this);
}
/** Our dirty field mask. */
protected transient FieldMask _dirty;
}
@@ -0,0 +1,35 @@
//
// $Id: UserExistsException.java,v 1.5 2001/09/21 03:01: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
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,336 @@
//
// $Id: UserManager.java,v 1.28 2003/12/05 18:20: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
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.Log;
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.StringUtil;
/**
* 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, boolean persist)
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, boolean persist)
throws AuthenticationFailedException
{
if (!user.passwordsMatch(password)) {
throw new InvalidPasswordException("error.invalid_password");
}
}
}
/**
* Constructs a user manager and prepares it 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 UserManager (Properties config, ConnectionProvider conprov)
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.");
}
// look up any override to our user auth cookie
String authCook = config.getProperty("auth_cookie.name");
if (!StringUtil.isBlank(authCook)) {
_userAuthCookie = authCook;
}
// register a cron job to prune the session table every hour
_pruner = new Interval() {
public void expired ()
{
try {
_repository.pruneSessions();
} catch (PersistenceException pe) {
Log.warning("Error pruning session table.");
Log.logStackTrace(pe);
}
}
};
_pruner.schedule(SESSION_PRUNE_INTERVAL, true);
}
/**
* 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);
}
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 authcode = CookieUtil.getCookieValue(req, _userAuthCookie);
if (authcode != null) {
return _repository.loadUserBySession(authcode);
} else {
return null;
}
}
/**
* 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 = StringUtil.replace(_loginURL, "%R", eurl);
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");
}
// potentially convert the user's legacy password
if (password != null && password.getCleartext() != null &&
user.updateLegacyPassword(password.getCleartext())) {
Log.info("Updated legacy password " + user.username + ".");
_repository.updateUser(user);
}
// run the user through the authentication gamut
auth.authenticateUser(user, username, password, persist);
// give them the necessary cookies and business
effectLogin(user, persist, req, rsp);
return user;
}
/**
* 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.
*/
public void effectLogin (User user, boolean persist,
HttpServletRequest req, HttpServletResponse rsp)
throws PersistenceException
{
// generate a new session for this user
String authcode = _repository.createNewSession(user, persist);
// stick it into a cookie for their browsing convenience
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("/");
// expire in one month if persistent, else at the end of the
// session
acookie.setMaxAge((persist) ? (30*24*60*60) : -1);
rsp.addCookie(acookie);
}
/**
* Logs the user out.
*/
public void logout (HttpServletRequest req, HttpServletResponse rsp)
{
String authcode = CookieUtil.getCookieValue(req, _userAuthCookie);
// nothing to do if they don't already have an auth cookie
if (authcode == null) {
return;
}
// set them up the bomb
Cookie c = new Cookie(_userAuthCookie, "x");
c.setPath("/");
c.setMaxAge(0);
CookieUtil.widenDomain(req, 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);
}
/** 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;
}
@@ -0,0 +1,617 @@
//
// $Id: UserRepository.java,v 1.40 2004/02/25 13:17:13 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
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 java.util.Properties;
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.StaticConnectionProvider;
import com.samskivert.jdbc.jora.Cursor;
import com.samskivert.jdbc.jora.FieldMask;
import com.samskivert.jdbc.jora.Session;
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)
throws PersistenceException
{
super(provider, USER_REPOSITORY_IDENT);
}
/**
* Derived classes that extend the user record with their own
* additional information will want to override this method and return
* their desired {@link User} derivation.
*/
protected Class getUserClass ()
{
return User.class;
}
// documentation inherited
protected void createTables (Session session)
{
// create our table object
_utable = new Table(
getUserClass().getName(), "users", session, "userId");
}
/**
* 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);
}
/**
* 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
{
execute(new Operation () {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
try {
_utable.insert(user);
// update the userid now that it's known
user.userId = liaison.lastInsertedId(conn);
// nothing to return
return null;
} catch (SQLException sqe) {
if (liaison.isDuplicateRowException(sqe)) {
throw new UserExistsException("error.user_exists");
} else {
throw sqe;
}
}
}
});
return user.userId;
}
/**
* 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 (final String sessionKey)
throws PersistenceException
{
return (User)execute(new Operation () {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
String query = "where authcode = '" + sessionKey +
"' AND sessions.userId = users.userId";
// look up the user
Cursor ec = _utable.select("sessions", query);
// fetch the user from the cursor
User user = (User)ec.next();
if (user != null) {
// call next() again to cause the cursor to close itself
ec.next();
// configure the user record with its field mask
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 loadUsersFromId (int[] userIds)
throws PersistenceException
{
// make sure we actually have something to do
if (userIds.length == 0) {
return new HashIntMap();
}
final String ids = genIdString(userIds);
return (HashIntMap)execute(new Operation () {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
// look up the users
Cursor ec = _utable.select("where userid in (" + ids + ")");
User user;
HashIntMap data = new HashIntMap();
while ((user = (User)ec.next()) != null) {
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 lookupUsersByEmail (String email)
throws PersistenceException
{
final String where = "where email = " +
JDBCUtil.escape(email);
return (ArrayList) execute(new Operation() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
return _utable.select(where).toArrayList();
}
});
}
/**
* Loads up a user record that matches the specified where clause.
* Returns null if no record matches.
*/
protected User loadUserWhere (final String whereClause)
throws PersistenceException
{
return (User)execute(new Operation () {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
// look up the user
Cursor ec = _utable.select(whereClause);
// fetch the user from the cursor
User user = (User)ec.next();
if (user != null) {
// call next() again to cause the cursor to close itself
ec.next();
// configure the user record with its field mask
user.setDirtyMask(_utable.getFieldMask());
}
return user;
}
});
}
/**
* 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;
}
execute(new Operation () {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
_utable.update(user, user.getDirtyMask());
// nothing to return
return null;
}
});
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;
}
execute(new Operation () {
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(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. Temporary
* sessions are set to expire at the end of the user's browser
* session. Persistent sessions expire after one month.
*/
public String createNewSession (final User user, boolean persist)
throws PersistenceException
{
// generate a random session identifier
final String authcode = UserUtil.genAuthCode(user);
// figure out when to expire the session
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, persist ? 30 : 2);
final Date expires = new Date(cal.getTime().getTime());
// insert the session into the database
execute(new Operation () {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
_session.execute("insert into sessions " +
"(authcode, userId, expires) values('" +
authcode + "', " + user.userId + ", '" +
expires + "')");
// nothing to return
return null;
}
});
// and let the user know what the session identifier is
return authcode;
}
/**
* Prunes any expired sessions from the sessions table.
*/
public void pruneSessions ()
throws PersistenceException
{
execute(new Operation () {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
_session.execute("delete from sessions where " +
"expires <= CURRENT_DATE()");
// nothing to return
return null;
}
});
}
/**
* 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.
* This is for Paul who whined about not knowing who was using Who,
* Where, When because he didn't feel like emailing anyone that wasn't
* already using it to link up.
*/
public String[] loadAllRealNames ()
throws PersistenceException
{
final ArrayList names = new ArrayList();
// do the query
execute(new Operation () {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
Statement stmt = _session.connection.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
String[] result = new String[names.size()];
names.toArray(result);
return result;
}
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];
}
final String ids = genIdString(userIds);
final HashIntMap map = new HashIntMap();
// do the query
execute(new Operation () {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
Statement stmt = _session.connection.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] = (String)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
StringBuffer ids = new StringBuffer();
for (int i = 0; i < userIds.length; i++) {
if (ids.length() > 0) {
ids.append(",");
}
ids.append(userIds[i]);
}
return ids.toString();
}
public static void main (String[] args)
{
Properties props = new Properties();
props.put(USER_REPOSITORY_IDENT + ".driver",
"org.gjt.mm.mysql.Driver");
props.put(USER_REPOSITORY_IDENT + ".url",
"jdbc:mysql://localhost:3306/samskivert");
props.put(USER_REPOSITORY_IDENT + ".username", "www");
props.put(USER_REPOSITORY_IDENT + ".password", "Il0ve2PL@Y");
try {
StaticConnectionProvider scp =
new StaticConnectionProvider(props);
UserRepository rep = new UserRepository(scp);
System.out.println(rep.loadUser("mdb"));
System.out.println(rep.loadUserBySession("auth"));
rep.createUser(new Username("samskivert"),
Password.makeFromClear("foobar"),
"Michael Bayne", "mdb@samskivert.com",
SiteIdentifier.DEFAULT_SITE_ID);
rep.createUser(new Username("mdb"),
Password.makeFromClear("foobar"), "Michael Bayne",
"mdb@samskivert.com",
SiteIdentifier.DEFAULT_SITE_ID);
scp.shutdown();
} catch (Throwable t) {
t.printStackTrace(System.err);
}
}
/** A wrapper that provides access to the userstable. */
protected Table _utable;
}
@@ -0,0 +1,76 @@
//
// $Id: UserUtil.java,v 1.10 2004/01/15 01:33:30 ray 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
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
StringBuffer buf = new StringBuffer();
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,58 @@
//
// $Id: Username.java,v 1.3 2004/01/31 13:03:39 mdb Exp $
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. Usernames must consist only of
* characters that match the following regular expression:
* <code>[_A-Za-z0-9]+</code> and be longer than two characters.
*/
public Username (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");
}
_username = username;
}
/** Returns the text of this username. */
public String getUsername ()
{
return _username;
}
/** Returns a string representation of this instance. */
public String toString ()
{
return _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,68 @@
//
// $Id: CookieUtil.java,v 1.3 2003/10/14 02:00:45 mdb Exp $
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 domain = req.getServerName();
int didx = domain.indexOf(".");
if (didx != -1) {
domain = domain.substring(didx);
}
cookie.setDomain(domain);
}
}
@@ -0,0 +1,35 @@
//
// $Id: DataValidationException.java,v 1.1 2001/10/31 09:44:22 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
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,174 @@
//
// $Id: ExceptionMap.java,v 1.2 2004/02/25 13:17:13 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
package com.samskivert.servlet.util;
import java.io.*;
import java.util.*;
import com.samskivert.Log;
import com.samskivert.util.ConfigUtil;
import com.samskivert.util.StringUtil;
/**
* 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();
_values = new ArrayList();
}
// 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
Properties loader = new Properties() {
public Object put (Object key, Object value)
{
_keys.add(key);
_values.add(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 < _keys.size(); i++) {
String exclass = (String)_keys.get(i);
try {
Class cl = Class.forName(exclass);
// replace the string with the class object
_keys.set(i, cl);
} catch (Throwable t) {
Log.warning("Unable to resolve exception class. " +
"[class=" + exclass +
", error=" + t + "].");
_keys.remove(i);
_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 = (Class)_keys.get(i);
if (cl.isInstance(ex)) {
msg = (String)_values.get(i);
break;
}
}
return StringUtil.replace(msg, MESSAGE_MARKER, ex.getMessage());
}
public static void main (String[] args)
{
ExceptionMap map = new ExceptionMap();
System.out.println(ExceptionMap.getMessage(new Exception("Test error")));
}
protected static List _keys;
protected static List _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: FriendlyException.java,v 1.1 2001/10/31 09:44:22 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
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,146 @@
//
// $Id: HTMLUtil.java,v 1.2 2002/12/30 04:52:36 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
package com.samskivert.servlet.util;
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 = StringUtil.replace(text, "&quot;", "\"");
text = StringUtil.replace(text, "&gt;", ">");
text = StringUtil.replace(text, "&lt;", "<");
text = StringUtil.replace(text, "&amp;", "&");
text = StringUtil.replace(text, "&", "&amp;");
text = StringUtil.replace(text, "<", "&lt;");
text = StringUtil.replace(text, ">", "&gt;");
text = StringUtil.replace(text, "\"", "&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 = StringUtil.replace(text, "\n\n", "\n<p>\n");
text = StringUtil.replace(text, "\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 = StringUtil.replace(text, "\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.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 tackle the *s
text = tbuf.toString();
m = _star.matcher(text);
tbuf.setLength(0);
while (m.find()) {
String match = m.group();
int start = m.start();
if (start == 0 || (start >= 2 && text.charAt(start-1) == '\n' &&
text.charAt(start-2) == '\n')) {
m.appendReplacement(tbuf, "<ul><li>");
} else {
m.appendReplacement(tbuf, "<li>");
}
}
m.appendTail(tbuf);
text = tbuf.toString();
// finally close the </ul>s and paragraphy
String[] paras = _blank.split(text);
tbuf.setLength(0);
for (int ii = 0; ii < paras.length; ii++) {
String para = paras[ii].trim();
if (para.startsWith("<ul>")) {
tbuf.append(para).append(" </ul>\n");
} else {
tbuf.append("<p> ").append(para).append(" </p>\n");
}
}
return tbuf.toString().trim();
}
protected static Pattern _url =
Pattern.compile("http://\\S+", Pattern.MULTILINE);
protected static Pattern _star = Pattern.compile("^\\*", Pattern.MULTILINE);
protected static Pattern _blank = Pattern.compile("^$", Pattern.MULTILINE);
}
@@ -0,0 +1,420 @@
//
// $Id: ParameterUtil.java,v 1.14 2004/02/25 13:17:13 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
package com.samskivert.servlet.util;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import javax.servlet.http.HttpServletRequest;
import com.samskivert.util.ArrayIntSet;
import com.samskivert.util.StringUtil;
/**
* Utility functions for fetching and manipulating request parameters
* (form fields).
*/
public class ParameterUtil
{
/**
* An interface for validating form parameters.
*/
public static interface ParameterValidator
{
public void validateParameter (String name, String value)
throws DataValidationException;
}
/**
* 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 ArrayIntSet getIntParameters (
HttpServletRequest req, String name, String invalidDataMessage)
throws DataValidationException
{
ArrayIntSet 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 HashSet.
*/
public static HashSet getParameters (HttpServletRequest req, String name)
throws DataValidationException
{
HashSet set = new HashSet();
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);
}
/**
* 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)) {
return null;
}
return parseDateParameter(value, invalidDataMessage);
}
/**
* 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 {
return _dparser.parse(value);
} catch (ParseException pe) {
throw new DataValidationException(invalidDataMessage);
}
}
/**
* Make 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;
}
// documentation inherited
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 SimpleDateFormat _dparser =
new SimpleDateFormat("yyyy-MM-dd");
}
@@ -0,0 +1,138 @@
//
// $Id: RequestUtils.java,v 1.10 2004/02/25 13:17:13 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
package com.samskivert.servlet.util;
import java.util.Iterator;
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();
Map map = req.getParameterMap();
if (map.size() > 0) {
buf.append("?");
for (Iterator iter = map.entrySet().iterator(); iter.hasNext(); ) {
Map.Entry entry = (Map.Entry)iter.next();
buf.append(entry.getKey()).append("=");
String[] values = (String[])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(")");
}
if (iter.hasNext()) {
buf.append("&");
}
}
}
return buf.toString();
}
}
@@ -0,0 +1,49 @@
//
// $Id: ServiceWaiter.java,v 1.4 2003/08/15 03:07:52 ray Exp $
package com.samskivert.servlet.util;
/**
* Extends the basic ServiceWaiter to be useful for servlets.
*/
public class ServiceWaiter extends com.samskivert.util.ServiceWaiter
{
/** 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,47 @@
//
// $Id$
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 implements ResultListener
{
/**
* Creates an AWT result listener that will dispatch results to the
* supplied target.
*/
public AWTResultListener (ResultListener target)
{
_target = target;
}
// documentation inherited from interface
public void requestCompleted (final Object 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 _target;
}
@@ -0,0 +1,161 @@
//
// $Id: AbsoluteLayout.java,v 1.1 2002/09/25 23:49:10 mdb Exp $
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 com.samskivert.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 _constraints = new HashMap();
}
@@ -0,0 +1,109 @@
//
// $Id: CollapsibleList.java,v 1.4 2002/05/20 18:06:07 shaper Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2002 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.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,173 @@
//
// $Id: CollapsiblePanel.java,v 1.8 2003/06/26 05:55:47 ray Exp $
package com.samskivert.swing;
import java.awt.EventQueue;
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 ()
{
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)
{
// 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() {
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(true);
}
/**
* 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,391 @@
//
// $Id: ComboButtonBox.java,v 1.5 2004/02/25 13:17:41 mdb Exp $
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());
}
// documentation inherited
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,88 @@
//
// $Id: CommandButton.java,v 1.2 2003/05/03 00:10:27 mdb Exp $
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;
}
// documentation inherited
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)
? arg = ((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,51 @@
//
// $Id: ControlledPanel.java,v 1.2 2002/12/18 04:19:16 mdb Exp $
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,422 @@
//
// $Id: Controller.java,v 1.18 2004/05/21 20:53:08 ray 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
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.JButton;
import javax.swing.JComponent;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import com.samskivert.Log;
import com.samskivert.swing.event.CommandEvent;
/**
* 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
* com.samskivert.swing.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. 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);
}
};
/**
* 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
* "handleExit" 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 handleCancelClicked (Object source);
* public void handleTextEntered (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 handleCancelClicked (JButton source);
* </pre>
*
* One would have to ensure that the only action events generated with
* the action command string "CancelClicked" were generated by
* <code>JButton</code> 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)
{
Method method = null;
Object[] args = null;
try {
// look for the appropriate method
String targetName = "handle" + action.getActionCommand();
Method[] methods = getClass().getMethods();
int mcount = methods.length;
for (int i = 0; i < mcount; i++) {
if (methods[i].getName().equals(targetName)) {
// see if we can generate the appropriate arguments
args = generateArguments(methods[i], action);
// 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 + ", event=" + 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 +
", event=" + action + "].");
Log.logStackTrace(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, ActionEvent action)
{
// 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[] { action.getSource() };
} else if (atypes.length == 2) {
if (action instanceof CommandEvent) {
CommandEvent command = (CommandEvent)action;
return new Object[] { action.getSource(),
command.getArgument() };
}
Log.warning("Unable to map non-command event to " +
"handler method that requires extra " +
"argument [controller=" + this +
", action=" + action + "].");
}
// we would have handled it, but we couldn't
return null;
}
/**
* 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);
button.setActionCommand(command);
button.addActionListener(DISPATCHER);
return button;
}
/**
* 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 + "].");
Log.logStackTrace(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: ControllerProvider.java,v 1.3 2001/08/14 00:59:19 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
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,66 @@
//
// $Id: DimenInfo.java,v 1.2 2001/08/11 22:43:28 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
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;
public String toString ()
{
StringBuffer buf = new StringBuffer();
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,63 @@
//
// $Id: DimmedIcon.java,v 1.1 2004/04/23 16:46:37 ray Exp $
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,80 @@
//
// $Id: EnablingAdapter.java,v 1.1 2002/10/23 02:06:14 mdb Exp $
package com.samskivert.swing;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JComponent;
import com.samskivert.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,490 @@
//
// $Id: GroupLayout.java,v 1.8 2004/02/25 13:17:41 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
package com.samskivert.swing;
import java.awt.*;
import javax.swing.JComponent;
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
{
int code;
public Policy (int code)
{
this.code = code;
}
}
/** A class used to make our policy constants type-safe. */
public static class Justification
{
int code;
public Justification (int code)
{
this.code = code;
}
}
/**
* 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(0);
/**
* Stretch all the widgets to their maximum possible size on this
* axis.
*/
public final static Policy STRETCH = new Policy(1);
/**
* Stretch all the widgets to be equal to the size of the largest
* widget on this axis.
*/
public final static Policy EQUALIZE = new Policy(2);
/**
* 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(3);
/** A justification constant. */
public final static Justification CENTER = new Justification(0);
/** A justification constant. */
public final static Justification LEFT = new Justification(1);
/** A justification constant. */
public final static Justification RIGHT = new Justification(2);
/** A justification constant. */
public final static Justification TOP = new Justification(3);
/** A justification constant. */
public final static Justification BOTTOM = new Justification(4);
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();
}
_constraints.put(comp, 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) _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 an {@link
* HGroupLayout} with a configuration conducive to containing a row of
* buttons.
*/
public static JPanel makeButtonBox (Justification justification)
{
return makeButtonBox(justification, null);
}
/**
* Creates a {@link JPanel} that is configured with an {@link
* HGroupLayout} with a configuration conducive to containing a row of
* buttons. The supplied button is added to the box.
*/
public static JPanel makeButtonBox (
Justification justification, JComponent button)
{
JPanel box = new JPanel(new HGroupLayout(NONE, justification));
if (button != null) {
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 _constraints;
protected static final int MINIMUM = 0;
protected static final int PREFERRED = 1;
protected static final int MAXIMUM = 2;
protected static final int DEFAULT_GAP = 5;
/** 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,60 @@
//
// $Id: GroupLayoutTest.java,v 1.3 2001/10/09 19:46: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
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 ()
{
public void windowClosing (WindowEvent e)
{
System.exit(0);
}
});
frame.getContentPane().add(panel, BorderLayout.CENTER);
frame.pack();
frame.setVisible(true);
}
}
@@ -0,0 +1,181 @@
//
// $Id: HGroupLayout.java,v 1.10 2003/04/10 21:43:13 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
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 ()
{
}
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;
}
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;
}
}
}
+334
View File
@@ -0,0 +1,334 @@
//
// $Id: IntField.java,v 1.8 2004/06/08 21:15:18 ray Exp $
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;
import com.samskivert.Log;
import com.samskivert.swing.util.SwingUtil;
/**
* 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() {
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);
}
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);
}
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
{
// documentation inherited
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 NumberFormat _formatter =
NumberFormat.getIntegerInstance();
}
@@ -0,0 +1,42 @@
//
// $Id: IntegerTableCellRenderer.java,v 1.2 2004/05/18 19:08:45 ray Exp $
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);
}
// documentation inherited
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,84 @@
//
// $Id: JInternalDialog.java,v 1.1 2002/07/09 17:48:23 ray Exp $
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;
}
+799
View File
@@ -0,0 +1,799 @@
//
// $Id: Label.java,v 1.37 2004/02/25 13:17:41 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2002 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.swing;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
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.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.SwingConstants;
import com.samskivert.Log;
import com.samskivert.util.RunAnywhere;
import com.samskivert.util.StringUtil;
import com.samskivert.util.Tuple;
/**
* 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}+)");
private static final Pattern ESCAPED_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
// _rawText will be null if there are no tags
_text = filterColors(text);
_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)
{
FontRenderContext frc = gfx.getFontRenderContext();
ArrayList 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();
layouts.add(new Tuple(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 tup = (Tuple)layouts.get(ii);
_layouts[ii] = (TextLayout)tup.left;
_lbounds[ii] = (Rectangle2D)tup.right;
// account for potential leaders
if (_lbounds[ii].getX() < 0) {
_leaders[ii] = (float)-_lbounds[ii].getX();
}
}
}
/**
* 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 ArrayList} or null if <code>keepWordsWhole</code>
* was true and the lines could not be layed out in the target width.
*/
protected ArrayList computeLines (
LineBreakMeasurer measurer, int targetWidth, Dimension size,
boolean keepWordsWhole)
{
// start with a size of zero
double width = 0, height = 0;
ArrayList layouts = new ArrayList();
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(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=" + t + "].");
Log.logStackTrace(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);
}
// 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);
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);
gfx.setColor(textColor);
layout.draw(gfx, rx + 1, y + 1);
} else if ((_style & SHADOW) != 0) {
textColor = gfx.getColor();
gfx.setColor(_alternateColor);
layout.draw(gfx, rx, y + 1);
gfx.setColor(textColor);
layout.draw(gfx, rx + 1, y);
} else if ((_style & BOLD) != 0) {
layout.draw(gfx, rx, y);
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 map = new HashMap();
map.put(TextAttribute.FONT, font);
if ((_style & UNDERLINE) != 0) {
map.put(TextAttribute.UNDERLINE,
TextAttribute.UNDERLINE_LOW_ONE_PIXEL);
}
AttributedString text = new AttributedString(_text, map);
// 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 {
try {
lastColor = new Color(Integer.parseInt(group, 16));
} catch (NumberFormatException nfe) {
Log.warning("This shouldn't be possible, the regex " +
"is precise [badcolor=" + group + "].");
}
}
// 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());
}
}
return text.getIterator();
}
/**
* 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;
// /** Used for debugging. */
// protected String _invalidator;
/** An approximation of the golden ratio. */
protected static final double GOLDEN_RATIO = 1.618034;
}
@@ -0,0 +1,195 @@
//
// $Id: LabelSausage.java,v 1.3 2003/05/14 01:52:52 ray Exp $
package com.samskivert.swing;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import javax.swing.Icon;
import com.samskivert.swing.util.SwingUtil;
/**
* An abstract lightweight renderer that sizes and renders a label (with
* optional icon) in a roundy-ended sausage.
*/
public abstract class LabelSausage
{
/**
* Constructs a label sausage.
*/
protected LabelSausage (Label label, Icon icon)
{
_label = label;
_icon = icon;
}
/**
* Lays out the label sausage. It is assumed that the desired label
* font is already set in the label.
*/
protected void layout (Graphics2D gfx, int extraPadding)
{
layout(gfx, 0, extraPadding);
}
/**
* Lays out the label sausage. It is assumed that the desired label
* font is already set in the label.
*
* @param iconPadding the number of pixels in the x direction to pad
* around the icon.
*/
protected void layout (Graphics2D gfx, int iconPadding, int extraPadding)
{
// if we have an icon, let that dictate our size; otherwise just
// lay out our label all on one line
int sqwid, sqhei;
if (_icon == null) {
sqwid = sqhei = 0;
} else {
sqwid = _icon.getIconWidth();
sqhei = _icon.getIconHeight();
_label.setTargetHeight(sqhei);
}
// lay out our label
_label.layout(gfx);
Dimension lsize = _label.getSize();
// if we have no icon, make sure that the label has enough room
if (_icon == null) {
sqhei = lsize.height + extraPadding * 2;
sqwid = extraPadding * 2;
}
// compute the diameter of the circle that perfectly encompasses our
// icon
int hhei = sqhei / 2;
int hwid = sqwid / 2;
_dia = (int) (Math.sqrt(hwid * hwid + hhei * hhei) * 2);
// compute the x and y offsets at which we'll start rendering
_xoff = (_dia - sqwid) / 2;
_yoff = (_dia - sqhei) / 2;
// and for the label
_lxoff = _dia - _xoff;
_lyoff = (_dia - lsize.height) / 2;
// now compute our closed and open sizes
_size.height = _dia;
// width is the diameter of the circle that contains
// the icon plus space for the label when we're open
_size.width = _dia + lsize.width + _xoff;
// and if we are actually rendering the icon, we need to
// account for the space between it and the label.
if (_icon != null) {
// and add the padding needed for the icon
_size.width += _xoff + (iconPadding * 2);
_xoff += iconPadding;
_lxoff += iconPadding * 2;
}
}
/**
* Paints the label sausage.
*/
protected void paint (
Graphics2D gfx, int x, int y, Color background, Object cliData)
{
// turn on anti-aliasing
Object oalias = SwingUtil.activateAntiAliasing(gfx);
// draw the base sausage
gfx.setColor(background);
drawBase(gfx, x, y);
// render our icon if we've got one
drawIcon(gfx, x, y, cliData);
drawLabel(gfx, x, y);
drawBorder(gfx, x, y);
drawExtras(gfx, x, y, cliData);
// restore original hints
SwingUtil.restoreAntiAliasing(gfx, oalias);
}
/**
* Draws the base sausage within which all the other decorations are
* added.
*/
protected void drawBase (Graphics2D gfx, int x, int y)
{
gfx.fillRoundRect(
x, y, _size.width - 1, _size.height - 1, _dia, _dia);
}
/**
* Draws the icon, if applicable.
*/
protected void drawIcon (Graphics2D gfx, int x, int y, Object cliData)
{
if (_icon != null) {
_icon.paintIcon(null, gfx, x + _xoff, y + _yoff);
}
}
/**
* Draws the label.
*/
protected void drawLabel (Graphics2D gfx, int x, int y)
{
_label.render(gfx, x + _lxoff, y + _lyoff);
}
/**
* Draws the black outer border.
*/
protected void drawBorder (Graphics2D gfx, int x, int y)
{
// draw the black outer border
gfx.setColor(Color.black);
gfx.drawRoundRect(x, y, _size.width - 1, _size.height - 1,
_dia, _dia);
}
/**
* Draws any extras that may be required.
*/
protected void drawExtras (Graphics2D gfx, int x, int y, Object cliData)
{
// nothing by default
}
/** The label. */
protected Label _label;
/** The optional icon. */
protected Icon _icon;
/** The size of this label sausage. */
protected Dimension _size = new Dimension();
/** The diameter of the circle that perfectly surrounds our icon. */
protected int _dia = 0;
/** The x offset of the icon. */
protected int _xoff = 0;
/** The y offset of the icon. */
protected int _yoff = 0;
/** The y offset for the label. */
protected int _lyoff = 0;
/** The x offset for the label. */
protected int _lxoff = 0;
}
@@ -0,0 +1,43 @@
//
// $Id: LabelStyleConstants.java,v 1.4 2003/11/15 03:17:28 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2002 Walter Korman
//
// 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;
/**
* Defines text style constants for use with the {@link Label} and {@link
* MultiLineLabel}.
*/
public interface LabelStyleConstants
{
/** Constant denoting normal text style. */
public static final int NORMAL = 0;
/** Constant denoting bold text style. */
public static final int BOLD = 1 << 0;
/** Constant denoting outline text style. */
public static final int OUTLINE = 1 << 1;
/** Constant denoting shadow text style. */
public static final int SHADOW = 1 << 2;
/** Constant denoting underline text style. */
public static final int UNDERLINE = 1 << 3;
}
@@ -0,0 +1,71 @@
//
// $Id: LazyComponent.java,v 1.1 2004/05/21 20:57:12 ray Exp $
package com.samskivert.swing;
import java.awt.BorderLayout;
import java.awt.Container;
import javax.swing.JComponent;
/**
* A component that doesn't actually create its content until it is
* visible and actually showing. This was made to support lazy-creation
* in JTabbedPane, since normally adding a compent to a JTabbedPane
* creates each component and their associated controllers.
*/
public class LazyComponent extends JComponent
{
/**
* An interface for creating the actual content that will live
* in this component.
*/
public interface ContentCreator
{
/**
* Create the content at the time that it is needed.
*/
public JComponent createContent ();
}
/**
* Create a lazy component.
*/
public LazyComponent (ContentCreator creator)
{
_creator = creator;
}
// documentation inherited
public void addNotify ()
{
super.addNotify();
checkCreate();
}
// documentation inherited
public void setVisible (boolean vis)
{
super.setVisible(vis);
checkCreate();
}
/**
* Check to see if we should now create the content.
*/
protected void checkCreate ()
{
if (_creator != null && isShowing()) {
// ideally, we would replace ourselves in our parent, but that
// doesn't seem to work in JTabbedPane
setLayout(new BorderLayout());
add(_creator.createContent(), BorderLayout.CENTER);
_creator = null;
}
}
/** The content creator. */
protected ContentCreator _creator;
}
@@ -0,0 +1,386 @@
//
// $Id: MultiLineLabel.java,v 1.14 2004/02/25 13:17:41 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2002 Walter Korman
//
// 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.Dimension;
import java.awt.Graphics2D;
import java.awt.Graphics;
import java.awt.RenderingHints;
import javax.swing.JComponent;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
/**
* A Swing component that displays a {@link Label}.
*/
public class MultiLineLabel extends JComponent
implements SwingConstants, LabelStyleConstants
{
/** A layout constant used by {@link #setLayout}. */
public static final int GOLDEN = HORIZONTAL+VERTICAL+1;
/** A layout constant used by {@link #setLayout}. */
public static final int NONE = GOLDEN+1;
/**
* Constructs an empty multi line label.
*/
public MultiLineLabel ()
{
this("");
}
/**
* Constructs a multi line label that displays the supplied text with
* center-alignment. The default layout is all on one line.
*
* @see #setLayout
*/
public MultiLineLabel (String text)
{
this(text, CENTER);
}
/**
* Constructs a multi line label that displays the supplied text with
* the specified alignment. The default layout is all on one line.
*
* @see #setLayout
*/
public MultiLineLabel (String text, int align)
{
this(text, align, NONE, 0);
}
/**
* Constructs a multi line label that displays the supplied text with
* the specified alignment. The default layout is all on one line.
*
* @see #setLayout
*/
public MultiLineLabel (String text, int align, int constrain, int size)
{
_label = new Label(text);
_label.setAlignment(align);
noteConstraints(constrain, size);
}
/**
* Sets whether this label's text should be rendered with
* anti-aliasing.
*/
public void setAntiAliased (boolean antialiased)
{
_antialiased = antialiased;
_dirty = true;
repaint();
}
/**
* Sets the constraints to be used when laying out the label.
*
* @param constrain {@link #HORIZONTAL} or {@link #VERTICAL} or {@link
* #GOLDEN} if the label should be laid out in a rectangle whose
* bounds approximate the golden ratio.
* @param size the width or height respectively to be targeted by the
* label or 0 if the label should react the first time it is laid out
* and use the dimension available at that point. <em>Note:</em> this
* requires that the label invalidate itself during its first
* validation which will cause it to change size visibly in the user
* interface. This argument is ignored if <code>constrain</code> is
* {@link #GOLDEN}.
*/
public void setLayout (int constrain, int size)
{
noteConstraints(constrain, size);
_dirty = true;
repaint();
}
/** Helper function. */
protected void noteConstraints (int constrain, int size)
{
switch (constrain) {
case HORIZONTAL:
if (size == 0) {
_constrain = HORIZONTAL;
} else {
_label.setTargetWidth(size);
}
break;
case VERTICAL:
if (size == 0) {
_constrain = VERTICAL;
} else {
_label.setTargetHeight(size);
}
break;
case GOLDEN:
_label.setGoldenLayout();
break;
case NONE:
// nothing doing
break;
default:
throw new IllegalArgumentException(
"Invalid constraint orientation " + constrain);
}
}
/**
* Sets the text displayed by this label.
*/
public void setText (String text)
{
if (_label.setText(text)) {
_dirty = true;
// clear out our constrained size where appropriate
if (_constrain == HORIZONTAL || _constrain == VERTICAL) {
_constrainedSize = 0;
_label.clearTargetDimens();
}
revalidate();
repaint();
}
}
/**
* Returns the text displayed by this label.
*/
public String getText ()
{
return _label.getText();
}
/**
* Sets the alternate color used to display the label text.
*/
public void setAlternateColor (Color color)
{
_label.setAlternateColor(color);
_dirty = true;
repaint();
}
/**
* Sets the alignment of the text displayed by this label.
*/
public void setAlignment (int align)
{
_label.setAlignment(align);
_dirty = true;
repaint();
}
/**
* Sets the off-axis alignment of the text displayed by this label.
*/
public void setOffAxisAlignment (int align)
{
_offalign = align;
_dirty = true;
repaint();
}
/**
* Sets the text style used to render this label.
*/
public void setStyle (int style)
{
_label.setStyle(style);
_dirty = true;
repaint();
}
// documentation inherited
public void paintComponent (Graphics g)
{
super.paintComponent(g);
// if we're dirty, re-lay things out before painting ourselves
if (_dirty) {
layoutLabel();
}
Graphics2D gfx = (Graphics2D)g;
int align = _label.getAlignment();
int dx = 0, dy = 0;
int wid = getWidth(), hei = getHeight();
Dimension ld = _label.getSize();
// set the text color to our foreground color
_label.setTextColor(getForeground());
// calculate the x-offset at which the label is rendered
switch (align) {
case CENTER: dx = (wid - ld.width) / 2; break;
case RIGHT: dx = wid - ld.width; break;
}
// calculate the y-offset at which the label is rendered
switch (_offalign) {
case CENTER: dy = (hei - ld.height) / 2; break;
case BOTTOM: dy = hei - ld.height; break;
}
// draw the label
_label.render(gfx, dx, dy);
}
// documentation inherited
public void doLayout ()
{
super.doLayout();
// if we have been configured to relay ourselves out once we know
// our constrained width or height, take care of that here
int size;
boolean delayedRevalidate = false;
switch (_constrain) {
case HORIZONTAL:
size = getWidth();
// sanity check; sometimes labels are laid out with completely
// invalid dimensions, so we just quietly play along
if (size > 0 && size != _constrainedSize) {
_constrainedSize = size;
_label.setTargetWidth(size);
delayedRevalidate = true;
}
break;
case VERTICAL:
size = getHeight();
if (size > 0 && size != _constrainedSize) {
_constrainedSize = size;
_label.setTargetHeight(size);
delayedRevalidate = true;
}
break;
}
// we can't just call revalidate() because we're in the middle of
// a validation traversal; our parent will, when we return from
// this method, declare itself to be valid; if we call
// revalidate() it will mark us and all of our parents as invalid,
// but then those of our parents that are involved in the first
// validation traversal will mark themselves as valid and in the
// validation pass that results from our call to revalidate() we
// will be skipped over; dooh! instead we delay a call to
// revalidate so that the current validation traversal will be
// completed before we mark ourselves and our parents as invalid
if (delayedRevalidate) {
Runnable callRevalidate = new Runnable() {
public void run() {
revalidate();
}
};
SwingUtilities.invokeLater(callRevalidate);
}
// go ahead and lay out the label in all cases so that we assume
// some sort of size
layoutLabel();
}
/**
* Called when the label has changed in some meaningful way and we'd
* accordingly like to re-layout the label, update our component's
* size, and repaint everything to suit.
*/
protected void layoutLabel ()
{
Graphics2D gfx = (Graphics2D)getGraphics();
if (gfx != null) {
// re-layout the label
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
(_antialiased) ?
RenderingHints.VALUE_ANTIALIAS_ON :
RenderingHints.VALUE_ANTIALIAS_OFF);
_label.layout(gfx);
gfx.dispose();
// note that we're no longer dirty
_dirty = false;
}
}
// documentation inherited
public Dimension getPreferredSize ()
{
if (_dirty) {
// attempt to lay out the label before obtaining its preferred
// dimensions
layoutLabel();
}
Dimension size = _label.getSize();
if (size != null) {
// never let our preferred size shrink in our constrained
// direction
switch (_constrain) {
case HORIZONTAL:
_prefd.width = Math.max(_prefd.width, size.width);
_prefd.height = size.height;
break;
case VERTICAL:
_prefd.width = size.width;
_prefd.height = Math.max(_prefd.height, size.height);
break;
default:
_prefd.width = size.width;
_prefd.height = size.height;
break;
}
}
return _prefd;
}
/** Our preferred size. */
protected Dimension _prefd = new Dimension(5, 5);
/** The label we're displaying. */
protected Label _label;
/** The off-axis alignment with which the label is positioned. */
protected int _offalign;
/** Pending constraint adjustments. */
protected int _constrain = NONE;
/** The size to which we constrained ourselves when most recently laid
* out. */
protected int _constrainedSize;
/** Whether to render the label with anti-aliasing. */
protected boolean _antialiased;
/** Whether this label is dirty and should be re-layed out. */
protected boolean _dirty = true;
}
@@ -0,0 +1,380 @@
//
// $Id: ObjectEditorTable.java,v 1.4 2004/06/15 17:57:17 ray Exp $
package com.samskivert.swing;
import java.awt.event.ActionListener;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.table.AbstractTableModel;
import com.samskivert.swing.event.CommandEvent;
import com.samskivert.util.ClassUtil;
import com.samskivert.util.CollectionUtil;
import com.samskivert.util.ListUtil;
import com.samskivert.Log;
/**
* Allows simple displaying and editing of Objects in a table format.
*/
public class ObjectEditorTable extends JTable
{
/**
* The default FieldInterpreter, which can be used to customize the
* name, values, and editing of a field in an Object.
*
* There are a number of ways that the field editing can be customized.
* A custom renderer (and editor) may be installed on the table, possibly
* in conjunction with overriding getClass(). Or you may simply override
* getValue (and setValue) to interpret between types, say for instance
* turning an integer field that may be one of three constant values into
* String names of the values.
*/
public static class FieldInterpreter
{
/**
* Get the name that she be used for the column header for the specified
* field. By default it's merely the name of the field.
*/
public String getName (Field field)
{
return field.getName();
}
/**
* Get the class of the specified field. By default, the class of
* the field is used, or its object equivalent if it is a primitive
* class.
*/
public Class getClass (Field field)
{
Class clazz = field.getType();
return ClassUtil.objectEquivalentOf(clazz);
}
/**
* Get the value of the specified field in the specified object.
* By default, the field is used to directly access the value.
*/
public Object getValue (Object obj, Field field)
{
try {
return field.get(obj);
} catch (IllegalAccessException iae) {
Log.logStackTrace(iae);
return null;
}
}
/**
* Set the value of the specified field in the specified object.
* By default, the field is used to directly set the value.
*/
public void setValue (Object obj, Object value, Field field)
{
try {
field.set(obj, value);
} catch (IllegalAccessException iae) {
Log.logStackTrace(iae);
}
}
}
/**
* Construct a table to display the specified class.
*
* @param protoClass the Class of the data that will be displayed.
*/
public ObjectEditorTable (Class protoClass)
{
this(protoClass, null);
}
/**
* Construct a table to display and edit the specified class.
*
* @param protoClass the Class of the data that will be displayed.
* @param editableFields the names of the fields that are editable.
*/
public ObjectEditorTable (Class protoClass, String[] editableFields)
{
this(protoClass, editableFields, null);
}
/**
* Construct a table to display and edit the specified class.
*
* @param protoClass the Class of the data that will be displayed.
* @param editableFields the names of the fields that are editable.
* @param interp The {@link FieldInterpreter} to use.
*/
public ObjectEditorTable (Class protoClass, String[] editableFields,
FieldInterpreter interp)
{
this(protoClass, editableFields, interp, null);
}
/**
* Construct a table to display and edit the specified class.
*
* @param protoClass the Class of the data that will be displayed.
* @param editableFields the names of the fields that are editable.
* @param interp The {@link FieldInterpreter} to use.
* @param displayFields the fields to display, or null to display all.
*/
public ObjectEditorTable (Class protoClass, String[] editableFields,
FieldInterpreter interp, String[] displayFields)
{
_interp = (interp != null) ? interp : new FieldInterpreter();
// figure out which fields we're going to display
Field[] fields = ClassUtil.getFields(protoClass);
if (displayFields != null) {
_fields = new Field[displayFields.length];
for (int ii=0; ii < displayFields.length; ii++) {
for (int jj=0; jj < fields.length; jj++) {
if (displayFields[ii].equals(fields[jj].getName())) {
_fields[ii] = fields[jj];
break;
}
}
if (_fields[ii] == null) {
throw new IllegalArgumentException(
"Field not found in prototype class! " +
"[class=" + protoClass +
", field=" + displayFields[ii] + "].");
}
}
} else {
_fields = fields; // use all the fields
}
// figure out which fields are editable
for (int ii=0, nn=_fields.length; ii < nn; ii++) {
if (ListUtil.contains(editableFields, _fields[ii].getName())) {
_editable.set(ii);
}
}
setModel(_model);
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
}
/**
* Set the data to be viewed or edited.
*/
public void setData (Object[] data)
{
_data.clear();
CollectionUtil.addAll(_data, data);
_model.fireTableDataChanged();
}
/**
* Set the data to be viewed or edited.
*/
public void setData (Collection data)
{
_data.clear();
_data.addAll(data);
_model.fireTableDataChanged();
}
/**
* Set this table to merely display / edit one lousy Object.
*/
public void setData (Object data)
{
_data.clear();
_data.add(data);
_model.fireTableDataChanged();
}
/**
* Add the specified element to the end of the data set.
*/
public void addDatum (Object element)
{
insertDatum(element, _data.size());
}
/**
* Insert the specified element at the specified row.
*/
public void insertDatum (Object element, int row)
{
_data.add(row, element);
_model.fireTableRowsInserted(row, row);
}
/**
* Change the value of the specified row.
*/
public void updateDatum (Object element, int row)
{
_data.set(row, element);
_model.fireTableRowsUpdated(row, row);
}
/**
* Remove the specified element from the set of data.
*/
public void removeDatum (Object element)
{
int dex = _data.indexOf(element);
if (dex != -1) {
removeDatum(dex);
}
}
/**
* Remove the specified row.
*/
public void removeDatum (int row)
{
_data.remove(row);
_model.fireTableRowsDeleted(row, row);
}
/**
* Get the edited data. Not really needed, since the the data being
* edited is the same objects that were passed in.
*/
public Object[] getData ()
{
return _data.toArray();
}
/**
* Get the currently selected object, or null if none selected.
*/
public Object getSelectedObject ()
{
int row = getSelectedRow();
return (row == -1) ? null : _data.get(row);
}
/**
* Add an action listener to this table.
*
* When any field is changed in the table, an action will be fired with
* a {@link CommandEvent}, with source being the table, the command
* being the name of the field that was updated, and the argument being
* the object that was updated. Note that no event is fired if a field
* was edited but the value did not change.
*/
public void addActionListener (ActionListener listener)
{
listenerList.add(ActionListener.class, listener);
}
/**
* Remove the specified action listener.
*/
public void removeActionListener (ActionListener listener)
{
listenerList.remove(ActionListener.class, listener);
}
/**
* A table model that uses the FieldInterpreter to muck with the objects.
*/
protected AbstractTableModel _model = new AbstractTableModel() {
/**
* Get the object at the specified row. Useful for our subclass.
*/
public Object getObjectAt (int row) {
return _data.get(row);
}
// documentation inherited
public int getColumnCount ()
{
return _fields.length;
}
// documentation inherited
public int getRowCount()
{
return _data.size();
}
// documentation inherited
public String getColumnName (int col)
{
return _interp.getName(_fields[col]);
}
// documentation inherited
public boolean isCellEditable (int row, int col)
{
return _editable.get(col);
}
// documentation inherited
public Class getColumnClass (int col)
{
return _interp.getClass(_fields[col]);
}
// documentation inherited
public Object getValueAt (int row, int col)
{
Object o = getObjectAt(row);
return _interp.getValue(o, _fields[col]);
}
// documentation inherited
public void setValueAt (Object value, int row, int col)
{
Object o = getObjectAt(row);
Object oldValue = _interp.getValue(o, _fields[col]);
// we only set the value if it has changed
if ((oldValue == null && value != null) ||
!oldValue.equals(value)) {
_interp.setValue(o, value, _fields[col]);
// fire the event
CommandEvent event = null;
Object[] listeners = ObjectEditorTable.this.listenerList
.getListenerList();
for (int ii=listeners.length-2; ii >= 0; ii -= 2) {
if (listeners[ii] == ActionListener.class) {
// lazy-create the event
if (event == null) {
event = new CommandEvent(ObjectEditorTable.this,
_fields[col].getName(), o);
}
((ActionListener) listeners[ii+1]).actionPerformed(
event);
}
}
}
}
};
/** The list of fields in the prototypical object. */
protected Field[] _fields;
/** An interpreter that is used to massage values in and out of the
* objects. */
protected FieldInterpreter _interp;
/** A list of flags corresponding to the _fields (and the table columns)
* that indicate if the field is editable. */
protected BitSet _editable = new BitSet();
/** The data being edited. */
protected ArrayList _data = new ArrayList();
}
@@ -0,0 +1,195 @@
//
// $Id: RadialLabelSausage.java,v 1.1 2003/04/15 20:28:36 mdb Exp $
package com.samskivert.swing;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Stroke;
import javax.swing.Icon;
import javax.swing.UIManager;
/**
* Specializes the label sausage to suit the needs of items placed within
* a radial menu. The colors used to render the radial label sausage must
* be configured via the {@link UIManager}. These colors are:
*
* <pre>
* RadialLabelSausage.activeBorder
* RadialLabelSausage.inactiveBorder
* RadialLabelSausage.background
* </pre>
*
* This would be configured in a custom look and feel like so:
*
* <pre>
* protected void initComponentDefaults (UIDefaults table)
* {
* super.initComponentDefaults(table);
* Object[] defaults = {
* "RadialLabelSausage.inactiveBorder", new ColorUIResource(0x478ABA),
* "RadialLabelSausage.activeBorder", new ColorUIResource(0xE0A000),
* "RadialLabelSausage.background", new ColorUIResource(0xD5E7E7),
* };
* table.putDefaults(defaults);
* }
* </pre>
*
* An application that is not using a custom look and feel can simply set
* these values with {@link UIManager#put}.
*/
public class RadialLabelSausage extends LabelSausage
{
/** The dimensions of this item when we are in our closed state as
* well as our most recently laid out coordinates. This is only valid
* after a call to {@link #layout}. */
public Rectangle closedBounds = new Rectangle();
/** The dimensions of this item when we are in open state. This is
* only valid after a call to {@link #layout}. */
public Rectangle openBounds = new Rectangle();
/**
* Constructs a radial label sausage.
*/
public RadialLabelSausage (String label, Icon icon)
{
super(new Label(label), icon);
}
/**
* Returns true if this menu item should be enabled when it is
* displayed. The default implementation always returns true.
*/
public boolean isEnabled (RadialMenu menu)
{
return true;
}
/**
* Sets whether the label is to be rendered as an active label.
*/
public void setActive (boolean active)
{
_active = active;
}
/**
* Computes the dimensions of this label based on the specified font
* and in the specified graphics context.
*/
public void layout (Graphics2D gfx, Font font)
{
_label.setFont(font);
layout(gfx, BORDER_THICKNESS);
openBounds.width = _size.width;
openBounds.height = _size.height;
// and closed up, we're just a circle
closedBounds.height = closedBounds.width = _size.height;
}
// documentation inherited
protected void drawLabel (Graphics2D gfx, int x, int y)
{
if (_active) {
super.drawLabel(gfx, x, y);
}
}
// documentation inherited
protected void drawBorder (Graphics2D gfx, int x, int y)
{
// then around all that draw the borders
Stroke ostroke = gfx.getStroke();
gfx.setStroke(BORDER_STROKE);
if (_active) {
// draw the active border
gfx.setColor(UIManager.getColor("RadialLabelSausage.activeBorder"));
gfx.drawRoundRect(
x + (BORDER_THICKNESS / 2), y + (BORDER_THICKNESS / 2),
openBounds.width - 1 - BORDER_THICKNESS,
openBounds.height - 1 - BORDER_THICKNESS,
_dia - BORDER_THICKNESS, _dia - BORDER_THICKNESS);
// draw the black outer border
gfx.setStroke(ostroke);
super.drawBorder(gfx, x, y);
} else {
// draw the inactive border
gfx.setColor(UIManager.getColor(
"RadialLabelSausage.inactiveBorder"));
gfx.drawOval(
x + (BORDER_THICKNESS / 2), y + (BORDER_THICKNESS / 2),
closedBounds.width - 1 - BORDER_THICKNESS,
closedBounds.height - 1 - BORDER_THICKNESS);
// draw the black outer border
gfx.setStroke(ostroke);
gfx.setColor(Color.black);
gfx.drawOval(x, y, closedBounds.width - 1, closedBounds.height - 1);
}
}
// documentation inherited
protected void drawExtras (Graphics2D gfx, int x, int y, Object cliData)
{
RadialMenu menu = (RadialMenu) cliData;
// finally, if we're dimmed out: dim us out
if (!isEnabled(menu)) {
Composite ocomp = gfx.getComposite();
gfx.setComposite(DISABLED_ALPHA);
gfx.setColor(Color.black);
drawBase(gfx, x, y);
gfx.setComposite(ocomp);
}
}
/**
* Draw the base circle or sausage within which all the other
* decorations are added.
*/
protected void drawBase (Graphics2D gfx, int x, int y)
{
if (_active) {
// render the sausage
super.drawBase(gfx, x, y);
} else {
// render the circle
gfx.fillOval(
x, y, closedBounds.width - 1, closedBounds.height - 1);
}
}
/**
* Paints the radial label sausage.
*/
protected void paint (Graphics2D gfx, int x, int y, RadialMenu menu)
{
paint(gfx, x, y, UIManager.getColor(
"RadialLabelSausage.background"), menu);
}
/** Indicates whether or not this label is active. */
protected boolean _active = false;
/** The thickness of the colored active/inactive border. */
protected static final int BORDER_THICKNESS = 4;
/** The stroke to use when drawing the selected/unselected color. */
protected static final Stroke BORDER_STROKE =
new BasicStroke(BORDER_THICKNESS);
/** The alpha level for how much we dim an inactive menu item by. */
protected static final Composite DISABLED_ALPHA =
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f);
}
@@ -0,0 +1,660 @@
//
// $Id: RadialMenu.java,v 1.6 2004/02/25 13:17:41 mdb Exp $
package com.samskivert.swing;
import java.awt.Component;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import com.samskivert.swing.event.CommandEvent;
import com.samskivert.swing.util.MouseHijacker;
import com.samskivert.swing.util.SwingUtil;
import com.samskivert.util.ObserverList;
import com.samskivert.util.RunAnywhere;
/**
* Provides a radial menu with iconic menu items that expand to include
* textual descriptions when moused over.
*/
public class RadialMenu
implements MouseMotionListener, MouseListener
{
/**
* The interface used to communicate back to the component that hosts
* this menu.
*/
public static interface Host
{
/**
* Returns the component on which we will be rendered.
*/
public Component getComponent ();
/**
* Requests the bounds of the visible region in which we'll be
* rendering the menu. The menu will be constrained to fit within
* these bounds starting with a natural position around the
* supplied target bounds and adjusting minimally to fit.
*/
public Rectangle getViewBounds ();
/**
* Requests that the appropriate region of the host be repainted.
*/
public void repaintRect (int x, int y, int width, int height);
/**
* Instructs the host component to stop rendering this menu
* because it has been popped down.
*/
public void menuDeactivated (RadialMenu menu);
}
/**
* Used to determine at the time that a menu is shown, whether its
* items should be included and/or enabled. Predicate methods are
* called immediately before showing a radial menu and thus should not
* perform complicated computations. They generally will check some
* simple model to determine a menu item's status.
*/
public static interface Predicate
{
/**
* If true, the menu will be included in the character menu when
* it is displayed.
*
* @param menu the menu that will include or not include the item
* in question.
* @param item the menu item that will or will not be included.
*/
public boolean isIncluded (RadialMenu menu, RadialMenuItem item);
/**
* If true, the menu will be enabled when displayed in a
* character's menu.
*
* @param menu the menu that contains the item in question.
* @param item the menu item that will or will not be enabled.
*/
public boolean isEnabled (RadialMenu menu, RadialMenuItem item);
}
/**
* An additional interface that can be implemented by the predicate
* to display extra status.
*/
public static interface IconPredicate extends Predicate
{
/**
* Return an additional predicate that should be drawn.
*/
public Icon getIcon (RadialMenu menu, RadialMenuItem item);
}
/**
* Constructs a radial menu.
*/
public RadialMenu ()
{
}
/**
* Adds a menu item to the menu. The menu should not currently be
* active.
*
* @param command the command to be issued when the item is selected.
* @param label the textual label to be displayed with the menu item.
* @param icon the icon to display next to the menu text or null if no
* icon is desired.
* @param predicate a predicate that will be used to determine whether
* or not this menu item should be included in the menu and enabled
* when it is shown.
*
* @return the item that was added to the menu. It can be modified
* while the menu is not being displayed.
*/
public RadialMenuItem addMenuItem (String command, String label,
Image icon, Predicate predicate)
{
RadialMenuItem item = new RadialMenuItem(
command, label, new ImageIcon(icon), predicate);
addMenuItem(item);
return item;
}
/**
* Adds an already constructed menu item to the menu. The menu should
* not currently be active.
*
* @param item the menu item instance to be added.
*/
public void addMenuItem (RadialMenuItem item, boolean makeDefault)
{
addMenuItem(item);
if (makeDefault) {
_defaultItem = item;
}
}
/**
* Adds an already constructed menu item to the menu. The menu should
* not currently be active.
*
* @param item the menu item instance to be added.
*/
public void addMenuItem (RadialMenuItem item)
{
_items.add(item);
}
/**
* Removes the specified menu item from this menu.
*/
public void removeMenuItem (RadialMenuItem item)
{
_items.remove(item);
}
/**
* Removes the first menu item that matches the specified command from
* this menu.
*
* @return true if a matching menu item was removed, false if not.
*/
public boolean removeMenuItem (String command)
{
int icount = _items.size();
for (int i = 0; i < icount; i++) {
RadialMenuItem item = (RadialMenuItem)_items.get(i);
if (item.command.equals(command)) {
_items.remove(i);
return true;
}
}
return false;
}
/**
* May be called by an item when it changes in a material way.
*/
public void itemUpdated ()
{
if (_host != null) {
layout();
repaint();
}
}
/**
* Adds a listener that will be notified when a menu item is
* selected. When the the menu is popped down, all listeners will be
* cleared.
*/
public void addActionListener (ActionListener listener)
{
_actlist.add(listener);
}
/**
* Sets the optional centerpiece icon to be displayed in the center of
* the menu.
*/
public void setCenterIcon (Icon icon)
{
_centerIcon = icon;
}
/**
* Activates the radial menu, rendering a menu around the prescribed
* bounds. It is expected that the host component will subsequently
* call {@link #render} if the menu invalidates the region of the
* component occupied by the menu and requests it to repaint.
*
* @param host the host component within which the radial menu is
* displayed.
* @param bounds the bounds of the object that was clicked to activate
* the menu.
* @param argument a reference to an object that will be provided
* along with the command that is issued if the user selects a menu
* item (unless that item has an overriding argument, in which case
* the overriding argument will be used).
*/
public void activate (Host host, Rectangle bounds, Object argument)
{
setActivationArgument(argument);
activate(host, bounds);
}
/**
* Activates the radial menu, rendering a menu around the prescribed
* bounds. It is expected that the host component will subsequently
* call {@link #render} if the menu invalidates the region of the
* component occupied by the menu and requests it to repaint.
*
* @param host the host component within which the radial menu is
* displayed.
* @param bounds the bounds of the object that was clicked to activate
* the menu.
*/
public void activate (Host host, Rectangle bounds)
{
_host = host;
_tbounds = bounds;
_poptime = System.currentTimeMillis();
_msMode = false;
Component comp = _host.getComponent();
_hijacker = new MouseHijacker(comp);
// start listening to the host component
comp.addMouseListener(this);
comp.addMouseMotionListener(this);
// lay ourselves out and repaint
layout();
repaint();
}
/**
* Sets the argument that will be defaultly posted along with commands.
*/
public void setActivationArgument (Object arg)
{
_argument = arg;
}
/**
* Returns the argument provided to this menu when {@link #activate}
* was called. This will only be non-null while the menu is active and
* only then if an argument was provided in the call to {@link
* #activate}.
*/
public Object getActivationArgument ()
{
return _argument;
}
/**
* Deactivates the menu.
*/
public void deactivate ()
{
if (_host != null) {
// unwire ourselves from the host component
Component comp = _host.getComponent();
comp.removeMouseListener(this);
comp.removeMouseMotionListener(this);
// reinstate the previous mouse listeners
if (_hijacker != null) {
_hijacker.release();
_hijacker = null;
}
// tell our host that we are no longer
_host.menuDeactivated(this);
// fire off a last repaint to clean up after ourselves
repaint();
}
// clear out our references
_host = null;
_tbounds = null;
_argument = null;
// clear out our action listeners
_actlist.clear();
}
/**
* Renders the current configuration of this menu.
*/
public void render (Graphics2D gfx)
{
Component host = _host.getComponent();
int x = _bounds.x, y = _bounds.y;
if (_centerLabel != null) {
// render the centerpiece label
_centerLabel.paint(gfx, x + _centerLabel.closedBounds.x,
y + _centerLabel.closedBounds.y, this);
}
// render each of our items in turn
int icount = _items.size();
for (int i = 0; i < icount; i++) {
RadialMenuItem item = (RadialMenuItem)_items.get(i);
if (!item.isIncluded(this)) {
continue;
}
// we have to wait and render the active item last
if (item != _activeItem) {
item.render(host, this, gfx, x + item.closedBounds.x,
y + item.closedBounds.y);
}
}
if (_activeItem != null) {
_activeItem.render(host, this, gfx, x + _activeItem.closedBounds.x,
y + _activeItem.closedBounds.y);
}
// render our bounds
// gfx.setColor(Color.green);
// gfx.draw(_bounds);
// gfx.setColor(Color.blue);
// gfx.draw(_tbounds);
}
// documentation inherited
public void mouseDragged (MouseEvent event)
{
// same as moved
mouseMoved(event);
}
// documentation inherited
public void mouseMoved (MouseEvent event)
{
int x = event.getX(), y = event.getY();
// translate the coords into our space
x -= _bounds.x;
y -= _bounds.y;
boolean repaint = false;
// see if we dragged out of the active item
if (_activeItem != null) {
// oldway: if (!_activeItem.openBounds.contains(x, y)) {
if (!_activeItem.closedBounds.contains(x, y)) {
_activeItem.setActive(false);
_activeItem = null;
repaint = true;
}
}
// see if we dragged into a new menu item
if ((_activeItem == null) &&
(RunAnywhere.getWhen(event) > _poptime + DEBOUNCE_DELAY)) {
int icount = _items.size();
for (int i = 0; i < icount; i++) {
RadialMenuItem item = (RadialMenuItem)_items.get(i);
if (item.isIncluded(this) &&
item.closedBounds.contains(x, y)) {
_activeItem = item;
_activeItem.setActive(true);
repaint = true;
// if we got here with a mouse moved, we've definately
// made the decision to be in non drag mode
if (event.getID() == MouseEvent.MOUSE_MOVED) {
_msMode = true;
}
break;
}
}
}
// repaint ourselves if something changed
if (repaint) {
repaint();
}
}
// documentation inherited
public void mouseClicked (MouseEvent event)
{
}
// documentation inherited
public void mouseEntered (MouseEvent event)
{
}
// documentation inherited
public void mouseExited (MouseEvent event)
{
}
// documentation inherited
public void mousePressed (MouseEvent event)
{
}
// documentation inherited
public void mouseReleased (MouseEvent event)
{
if (_activeItem == null) {
long when = RunAnywhere.getWhen(event);
if (!_msMode &&
(when < _poptime + DRAGMODE_DELAY)) {
// if the dragmode delay time hasn't passed, then we're
// going to leave the menu up because the user wants it
// to behave like a MS Windows menu.
_msMode = true;
return;
// if they've double clicked, then activate the default item
} else if (_msMode && (when < _poptime + DOUBLECLICK_DELAY)) {
_activeItem = _defaultItem;
}
}
// deactivate the active item and post a command for it
if (_activeItem != null) {
// only process the selection if the item is enabled
if (_activeItem.isEnabled(this) && _activeItem.command != null) {
// if the item has an overriding argument, use that
Object itemArg = _activeItem.argument;
Object arg = (itemArg == null) ? _argument : itemArg;
// notify our listeners that the action is posted
final CommandEvent evt = new CommandEvent(
_host.getComponent(), _activeItem.command, arg);
_actlist.apply(new ObserverList.ObserverOp() {
public boolean apply (Object obs) {
((ActionListener)obs).actionPerformed(evt);
return true;
}
});
}
_activeItem.setActive(false);
_activeItem = null;
}
// deactive ourselves
deactivate();
}
/**
* Lays the radial menu items out based on the currently configured
* bounds information.
*/
protected void layout ()
{
Graphics2D gfx = (Graphics2D)_host.getComponent().getGraphics();
Font font = gfx.getFont();
_bounds = new Rectangle();
// figure out which items are included
int icount = _items.size();
ArrayList items = new ArrayList();
for (int i = 0; i < icount; i++) {
RadialMenuItem item = (RadialMenuItem)_items.get(i);
if (item.isIncluded(this)) {
items.add(item);
}
}
// lay out all of our menu items
int maxwid = 0, maxhei = 0;
icount = items.size();
for (int i = 0; i < icount; i++) {
RadialMenuItem item = (RadialMenuItem)items.get(i);
item.layout(gfx, font);
// track maximum menu item size
if (item.closedBounds.width > maxwid) {
maxwid = item.closedBounds.width;
}
if (item.closedBounds.height > maxhei) {
maxhei = item.closedBounds.height;
}
}
gfx.dispose();
// use the maximum of either width or height and make a circle
// around that
double radius = Math.max(_tbounds.height, _tbounds.width) / 2;
// be sure to add a gap and space for the menu item itself
radius += (5 + maxwid/2);
// compute the angle between menu items (we use the diameter of
// the menu items as an approximate measure of the distance along
// the circumference)
double theta = (maxwid + 10) / radius ;
// now position each item accordingly
double angle = -Math.PI/2;
for (int i = 0; i < icount; i++) {
RadialMenuItem item = (RadialMenuItem)items.get(i);
int ix = (int)(radius * Math.cos(angle));
int iy = (int)(radius * Math.sin(angle));
item.openBounds.x = item.closedBounds.x = ix - maxwid/2;
item.openBounds.y = item.closedBounds.y = iy - maxhei/2;
// move along the circle
angle += theta;
}
// create and position the centerpiece label
if (_centerIcon != null) {
_centerLabel = new RadialLabelSausage("", _centerIcon);
_centerLabel.layout(gfx, font);
_centerLabel.openBounds.x = _centerLabel.closedBounds.x =
-(_centerLabel.closedBounds.width / 2);
_centerLabel.openBounds.y = _centerLabel.closedBounds.y =
-(_centerLabel.closedBounds.height / 2);
}
// now compute the rectangle that encloses the entire menu
// include the bounds for the centerpiece label
if (_centerLabel != null) {
_bounds.add(_centerLabel.openBounds);
}
// include the bounds for all menu items
for (int i = 0; i < icount; i++) {
RadialMenuItem item = (RadialMenuItem)items.get(i);
// we need the open bounds rather than the closed ones
_bounds.add(item.openBounds);
}
// now translate everything from the center of the target bounds
// to the upper left of the menu bounds
if (_centerLabel != null) {
_centerLabel.openBounds.translate(-_bounds.x, -_bounds.y);
_centerLabel.closedBounds.translate(-_bounds.x, -_bounds.y);
}
for (int i = 0; i < icount; i++) {
RadialMenuItem item = (RadialMenuItem)items.get(i);
item.openBounds.translate(-_bounds.x, -_bounds.y);
item.closedBounds.translate(-_bounds.x, -_bounds.y);
}
// the origin was at the center of the encircled rectangle; we
// need to translate it such that the origin of our bounds are in
// screen coordinates and at the upper left rather than the center
_bounds.x += (_tbounds.x + _tbounds.width/2);
_bounds.y += (_tbounds.y + _tbounds.height/2);
// now make sure the whole shebang is fully visible within the
// host component
Rectangle hbounds = _host.getViewBounds();
Point pos = SwingUtil.fitRectInRect(_bounds, hbounds);
_bounds.setLocation(pos);
}
/**
* Requests that our host component repaint the part of itself that we
* occupy.
*/
protected void repaint ()
{
// only repaint the area that we overlap
_host.repaintRect(_bounds.x, _bounds.y,
_bounds.width+1, _bounds.height+1);
}
/** Our host component. */
protected Host _host;
/** The bounds around which we're rendering a menu. */
protected Rectangle _tbounds;
/** The bounds that all of our menu items occupy. */
protected Rectangle _bounds;
/** The argument object, which will be delivered along with a posted
* command when a menu item is selected. */
protected Object _argument;
/** The centerpiece icon or null if there is none. */
protected Icon _centerIcon;
/** The label used to render the centerpiece icon if there is one. */
protected RadialLabelSausage _centerLabel;
/** Our menu items. */
protected ArrayList _items = new ArrayList();
/** The item that has the mouse over it presently, and the default item
* if we're double clicked. */
protected RadialMenuItem _activeItem, _defaultItem;
/** The amount of time the user must hold down the mouse in order
* to put it in drag mode, after which the first mouse-up will pop
* down the menu. */
protected static final long DRAGMODE_DELAY = 1000L;
/** The amount of time after the menu has been popped up before we
* accept selections. */
protected static final long DEBOUNCE_DELAY = 200L;
/** How quickly a second click must arrive for us to count it as a double
* click and not two single clicks. */
protected static final long DOUBLECLICK_DELAY = 400L;
/** The time at which the menu was popped up. */
protected long _poptime;
/** Whether we're popping the menu up in microsoft mode, where the
* user does not need to drag to the item they want to select. */
protected boolean _msMode = false;
/** This hijacks and holds onto the previous mouse listeners. */
protected MouseHijacker _hijacker;
/** Maintains a list of action listeners. */
protected ObserverList _actlist = new ObserverList(
ObserverList.SAFE_IN_ORDER_NOTIFY);
}
@@ -0,0 +1,121 @@
//
// $Id: RadialMenuItem.java,v 1.2 2004/02/25 13:17:41 mdb Exp $
package com.samskivert.swing;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics2D;
import javax.swing.Icon;
/**
* Used to track info for each menu item in a {@link RadialMenu}.
*/
public class RadialMenuItem extends RadialLabelSausage
{
/** The command to issue if this item is selected. */
public String command;
/** A special argument to be used when this menu item is selected,
* rather than the argument provided to the radial menu when it was
* activated. */
public Object argument;
/** Used to determine whether or not this menu item should be included
* in a menu and whether or not it should be enabled. If no predicate
* is available, a menu item is assumed always to be included and
* enabled. */
public RadialMenu.Predicate predicate;
/**
* Constructs a radial menu item with the specified command and label.
* No icon or menu predicate will be used.
*/
public RadialMenuItem (String command, String label)
{
this(command, label, null, null);
}
/**
* Constructs a radial menu item with the specified command, label and
* icon. No menu predicate will be used.
*/
public RadialMenuItem (String command, String label, Icon icon)
{
this(command, label, icon, null);
}
/**
* Constructs a radial menu item with the specified command, label and
* icon.
*/
public RadialMenuItem (String command, String label, Icon icon,
RadialMenu.Predicate predicate)
{
super(label, icon);
_label.setTextColor(Color.black);
this.command = command;
this.predicate = predicate;
}
/**
* Returns true if this menu item should be included in a menu when it
* is displayed. Calls through to the {@link RadialMenu.Predicate} if
* we have one.
*/
public boolean isIncluded (RadialMenu menu)
{
return (predicate == null || predicate.isIncluded(menu, this));
}
/**
* Returns true if this menu item should be enabled when it is
* displayed. Calls through to the {@link RadialMenu.Predicate} if we
* have one.
*/
public boolean isEnabled (RadialMenu menu)
{
return (predicate == null || predicate.isEnabled(menu, this));
}
/**
* Menu items are equal if their commands are equal. We also
* declare ourselves to be equal to a string with the same value
* as our command.
*/
public boolean equals (Object other)
{
if (other instanceof RadialMenuItem) {
return command.equals(((RadialMenuItem)other).command);
} else if (other instanceof String) {
return command.equals(other);
} else {
return false;
}
}
/**
* Renders this menu item at the specified location.
*/
public void render (
Component host, RadialMenu menu, Graphics2D gfx, int x, int y)
{
paint(gfx, x, y, menu);
}
// documentation inherited
protected void drawIcon (Graphics2D gfx, int x, int y, Object cliData)
{
super.drawIcon(gfx, x, y, cliData);
if (predicate instanceof RadialMenu.IconPredicate) {
Icon icon = ((RadialMenu.IconPredicate) predicate).getIcon(
(RadialMenu) cliData, this);
if (icon != null) {
icon.paintIcon(null, gfx, x + _xoff, y + _yoff);
}
}
}
}

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