Maven, JRuby-Complete and Rake
In a previous post I described how to configure Maven to use JRuby to perform a build using Rake from within a Maven build process. While running that same process on a newly re-imaged windows box I encountered the following error output:
jruby: No such file, directory, or command -- rake
This confused me for some time, I’m using jruby-complete which packages rake within itself, how could it not be able to find its own rake? Eventually it occurred to me that the only difference between my new and old windows images was the location of my maven repository. My old image had the repository manually configured to be in a folder which has no spaces in its path, my new image just had the default setting, which placed the repository in c:\Documents And Settings\username\.m2\repository. I manually configured Maven to store the repository in a path with no spaces and hey presto the problem went away.
So this is just a short note to remind myself when this problem next crops up: even now in the second decade of the 21st century avoid spaces in file names!!!
links for 2009-09-01
Comments Off
Recording API Invocations for Debugging
Today I encountered a problem when using the Oracle JDBC API that only manifested itself in a certain complicated sequence of API invocations. I wanted to understand exactly what the sequence of API invocations was and also be able to extract them, so I could spit them out into a standalone test-case that I could use to debug the problem. Below is a class I whipped up to help automate most of this task.
So now I can do something like:
Invocations t = new Invocations();
PoolDataSource ods= t.trace(PoolDataSourceFactory.getPoolDataSource(),PoolDataSource.class);
ods.setConnectionFactoryClassName("oracle.jdbc.pool.OracleDataSource");
ods.setURL("jdbc:oracle:thin://localhost:1521/orcl");
ods.setUser("scott");
ods.setPassword("tiger");
Connection conn = ods.getConnection();
CallableStatement stmt = conn.prepareCall("begin dbms_output.put_line('Hello'); end;");
stmt.execute();
stmt.close();
conn.close();
System.out.print(t);
Which produces the following output:
oracle.ucp.jdbc.PoolDataSource v1;
v1.setConnectionFactoryClassName("oracle.jdbc.pool.OracleDataSource");
v1.setURL("jdbc:oracle:thin://localhost:1521/orcl");
v1.setUser("scott");
v1.setPassword("tiger");
java.sql.Connection v2;
v2 = v1.getConnection();
java.sql.CallableStatement v3;
v3 = v2.prepareCall("begin dbms_output.put_line('Hello'); end;");
boolean v4;
v4 = v3.execute();
v3.close();
v2.close();
Which is a pretty good facsimile of the original code. Note there are obviously limitations to what this kind of approach can achieve:
Invocationscan only handle primitive values and values created within the API itself- Any non primitive values (including arrays) created outside of the API can only be declared, code to initialize these values will need to be added manually.
The major benefit of the generated code is that it does not have any dependencies other than the API being traced, thus making it easier to extract a stand-alone test case that exhibits the problem. This makes it easier to focus on the problem by eradicating superfluous code. It also very useful when you need a stand-alone test-case for a bug report for a third party API.
The Source Code
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.IdentityHashMap;
public class Invocations {
@Override
public String toString() {
return b.toString();
}
public <T> T trace(final Object target, final Class<T> type) {
return trace(target, type, false);
}
private void declare(final Object object, final Class<?> type,
final boolean force) {
if (!void.class.equals(type)) {
if (force
|| (object != null && !variables.containsKey(object) && !isPrimitive(type))) {
b.append(typeName(type));
b.append(" ");
b.append(register(object));
b.append(";\n");
}
}
}
private boolean isPrimitive(final Class<?> type) {
boolean isPrimitive = type.isPrimitive();
if (!isPrimitive) {
for (final Class<?> c : PRIMITIVES) {
if (c.equals(type)) {
isPrimitive = true;
break;
}
}
}
return isPrimitive;
}
private String name(final Object object) {
String name = "null";
if (object != null) {
name = variables.get(object);
if (name == null) {
if (isPrimitive(object.getClass())) {
return render(object);
} else {
throw new RuntimeException("Could not find name for: "
+ object);
}
}
}
return name;
}
private String register(final Object object) {
String name;
name = variables.get(object);
if (name == null) {
name = "v" + (++var);
variables.put(object, name);
}
return name;
}
private String render(final Object object) {
final Class<?> type = object.getClass();
if (String.class.equals(type)) {
return "\"" + object.toString() + "\"";
} else if (short.class.equals(type) || Short.class.equals(type)) {
return "(short)" + object.toString();
} else if (long.class.equals(type) || Long.class.equals(type)) {
return object.toString() + "L";
} else if (char.class.equals(type) || Character.class.equals(type)) {
return "'" + object.toString() + "'";
} else {
return object.toString();
}
}
@SuppressWarnings("unchecked")
private <T> T trace(final Object target, final Class<T> type,
final boolean force) {
T result = null;
if (target != null) {
if (target.getClass().getInterfaces().length > 0
&& !isPrimitive(target.getClass())) {
result = type.cast(Proxy.newProxyInstance(Thread
.currentThread().getContextClassLoader(), target
.getClass().getInterfaces(), new Recorder(target)));
} else {
result = (T) target;
}
}
declare(result, type, force);
return result;
}
private String typeName(final Class<?> type) {
if (type.isArray()) {
return typeName(type.getComponentType()) + "[]";
} else {
return type.getName();
}
}
private class Recorder implements InvocationHandler {
Recorder(final Object target) {
this.target = target;
}
public Object invoke(final Object proxy, final Method method,
final Object[] args) throws Throwable {
Object result = null;
final Class<?> type = method.getReturnType();
try {
result = method.invoke(target, args);
} catch (final InvocationTargetException e) {
final Throwable t = e.getCause();
for (final Class<?> ex : method.getExceptionTypes()) {
if (ex.isAssignableFrom(t.getClass())) {
throw t;
}
}
throw e;
}
result = trace(result, type, true);
record(result, proxy, method, args);
return result;
}
private void record(final Object result, final Object proxy,
final Method method, final Object[] args) {
if (args != null) {
final Class<?>[] types = method.getParameterTypes();
for (int i = 0; i < args.length; ++i) {
declare(args[i], types[i], false);
}
}
if (result != null) {
b.append(name(result));
b.append(" = ");
}
b.append(name(proxy));
b.append('.');
b.append(method.getName());
b.append('(');
if (args != null) {
for (int i = 0; i < args.length; ++i) {
b.append(name(args[i]));
if (i < args.length - 1) {
b.append(',');
}
}
}
b.append(");\n");
}
private final Object target;
};
private final StringBuilder b = new StringBuilder();
private int var = 0;
private final IdentityHashMap<Object, String> variables = new IdentityHashMap<Object, String>();
private static final Class<?>[] PRIMITIVES = { String.class, Integer.class,
Boolean.class, Double.class, Long.class, Short.class, Byte.class,
Character.class, Float.class };
}
I’ve only used this code with the specific problem I was facing, I’m sure there are issues with it, I haven’t tested it comprehensively, YMMV. For example I know it doesn’t handle arrays very well, particularly those with multiple dimensions.
Maven, JRuby and Rails Migrations
Here’s how I managed to get Rails Migrations working with a Maven build process, enabling me to leverage Migrations to maintain the RDBMS schema of a Java application.
Add JRuby and Database dependencies
Add the following dependencies to your pom.xml (adjust for whatever is latest version of jruby and whatever your database is).
<dependency> <groupId>org.jruby</groupId> <artifactId>jruby-complete</artifactId> <version>1.3.0RC1</version> </dependency> <dependency> <groupId>oracle</groupId> <artifactId>jdbc</artifactId> <version>11.1.0</version> </dependency>
Add a goal to install required Ruby gems
To get Rails migrations running we’ll need the following gems:
- activerecord
- activerecord-jdbc-adapter
- your database specific jdbc adapter
We’ll set up a profile to install these gems if they are missing (based on a approach used by Binil Thomas):
<profiles>
<profile>
<id>first.time</id>
<activation>
<file>
<missing>${user.home}/.gem/jruby/1.8/gems/activerecord-2.3.2</missing>
</file>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>install-active-record-gem</id>
<phase>initialize</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>org.jruby.Main</mainClass>
<arguments>
<argument>-S</argument>
<argument>gem</argument>
<argument>install</argument>
<argument>activerecord</argument>
<argument>activerecord-jdbc-adapter</argument>
<argument>activerecord-oracle_enhanced-adapter</argument>
<argument>--no-ri</argument>
<argument>--no-rdoc</argument>
<argument>--no-test</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
Replace activerecord-oracle_enhanced-adapter with the name of your chosen database’s jdbc adapter.
Add a goal to run the migration
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>run-migrate</id>
<phase>install</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>org.jruby.Main</mainClass>
<arguments>
<argument>-S</argument>
<argument>rake</argument>
<argument>-f</argument>
<argument>${basedir}/Rakefile</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Create a Rakefile
You’ll need a Rakefile to carry out the migration, in my case I wanted to avoid depending on all of Rails so I just used the following:
require 'active_record'
require 'yaml'
RAILS_ROOT = "#{File.dirname(__FILE__)}" unless defined?(RAILS_ROOT)
task :default => :migrate
task :migrate => :environment do
ActiveRecord::Migrator.migrate('db/migrate', ENV["VERSION"] ? ENV["VERSION"].to_i : nil )
end
task :environment do
ActiveRecord::Base.establish_connection(YAML::load(File.open("#{RAILS_ROOT}/config/database.yml")))
ActiveRecord::Base.logger = Logger.new(File.open('database.log', 'a'))
end
Write your migrations
Create your migrations in db/migrate.
Apply database changes
Finally type:
mvn install
to deploy your database changes.
The full POM
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>schema</artifactId>
<name>Example POM for doing Rails Migrations</name>
<packaging>pom</packaging>
<dependencies>
<dependency>
<groupId>org.jruby</groupId>
<artifactId>jruby-complete</artifactId>
<version>1.3.0RC1</version>
</dependency>
<dependency>
<groupId>oracle</groupId>
<artifactId>jdbc</artifactId>
<version>11.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>run-migrate</id>
<phase>install</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>org.jruby.Main</mainClass>
<arguments>
<argument>-S</argument>
<argument>rake</argument>
<argument>-f</argument>
<argument>${basedir}/Rakefile</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>first.time</id>
<activation>
<file>
<missing>${user.home}/.gem/jruby/1.8/gems/activerecord-2.3.2</missing>
</file>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>install-active-record-gem</id>
<phase>initialize</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>org.jruby.Main</mainClass>
<arguments>
<argument>-S</argument>
<argument>gem</argument>
<argument>install</argument>
<argument>activerecord</argument>
<argument>activerecord-jdbc-adapter</argument>
<argument>activerecord-oracle_enhanced-adapter</argument>
<argument>--no-ri</argument>
<argument>--no-rdoc</argument>
<argument>--no-test</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Comments Off
Where is osso1013?
When setting up single sign on for an Oracle Application Server 10.1.3.1 instance, I need to run the osso1013 script, and I could not recall its location, so for future reference it is:
$ORACLE_HOME/Apache/Apache/bin/osso1013
where $ORACLE_HOME is the location of the Oracle Application Server installation
Comments Off
Add Oracle JDBC Jar to Maven Repository
If a jar is not available from a public repository (for example, Oracle JDBC jars), then you can get Maven to manually install a copy of the jar in the local repository. To manually deploy the Oracle JDBC driver to your local repository type the following:
mvn install:install-file -Dfile={ORACLE_HOME}/jdbc/lib/ojdbc6.jar -Dpackaging=jar\
-DgroupId=com.oracle -DartifactId=ojdbc6 -Dversion=11.1.0
where {ORACLE_HOME} is the path to the Oracle Database installation.
You can then reference the the jar via a dependency declaration like the following:
<dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc6</artifactId> <version>11.1.0</version> </dependency>
Comments Off
Remote Debug JRuby
$ jruby -J-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=4000 -J-Xdebug ...
Comments Off
Comments Off