/*
* UGenInfo.java
* JCollider
*
* Copyright (c) 2004-2010 Hanns Holger Rutz. All rights reserved.
*
* This software is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either
* version 2, june 1991 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License (gpl.txt) along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*
* For further information, please contact Hanns Holger Rutz at
* contact@sciss.de , or visit http://www.sciss.de/jcollider
*
*
* JCollider is closely modelled after SuperCollider Language,
* often exhibiting a direct translation from Smalltalk to Java.
* SCLang is a software originally developed by James McCartney,
* which has become an Open Source project.
* See http://supercollider.sourceforge.net/ for details.
*
*
* Changelog:
* 04-Sep-05 created
* 11-Feb-08 supports binary definition files
*/
package de.sciss.jcollider;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.InputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.RandomAccessFile;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
* As stated elsewhere, it was decided to not implement separate
* classes for the different unit generators in JCollider. Instead
* there is one monolithic <code>UGen</code> class. To facilitate
* the visual represenation of UGens and to allow syntax checking
* and automatic default value assignment, the <code>UGenInfo</code>
* class was created which keeps records of all known ugen
* "classes". The database needs to be read in once
* explictly, usually you will do this once your programme launches.
* The database is merged with the JCollider library .jar file
* (<code>ugendefs.xml</code>, along with the document type
* descriptor <code>ugendefs.dtd</code>), and can be easily
* edited as to provide more ugens newly added to the supercollider
* application. This database file would also naturally be the
* place to put new attributes such as icon files if you want
* to build a graphic synthesis system, or links to help file and such.
* <p>
* Once you have initialized the database, using the <code>readDefinitions</code>
* method, you don't deal with <code>UGenInfo</code> any more, since
* the UGen constructor methods will query the database automatically.
* This in turn means, you cannot use <code>UGen.ar( ... )</code> for example,
* unless the database has been initialized. However there is nothing
* wrong with using JCollider without the UGen constructors (for example
* using only pre-stored synth defs), and in this case you may skip
* the database initialization, saving some startup time.
*
* @author Hanns Holger Rutz
* @version 0.32, 25-Feb-08
*
* @see #readDefinitions
* @see UGen#ar( String )
*/
public class UGenInfo
implements Constants, Comparable
{
private static final String UGENDEFS_DTD = "ugendefs.dtd";
private static final EntityResolver dtdResolver = new DTDResolver();
private static final int BINARY_FILE_COOKIE = 0x7567656E; // "ugen"
private static final int BINARY_FILE_VERSION = 0;
/**
* This field contains a read-only map
* which maps String (ugen class names and
* alias names such as the unary/binary operator names)
* to <code>UGenInfo</code> elements).
* <p>
* For example <code>UGenInfo.infos.get( "PlayBuf" )</code>
* will return the dataset for the PlayBuf ugen,
* <code>UGenInfo.infos.get( "reciprocal" )</code>
* will return the dataset for the UnaryOpUGen ugen
* (there is only one dataset for all the operators).
*/
public static Map infos;
/**
* Value for <code>outputType</code> : the ugen
* has a fixed number of outputs
*/
public static final int OUTPUT_FIXED = 0; // a constant value
/**
* Value for <code>outputType</code> : the ugen
* has a variable number which needs to be
* specified explictly when constructing the
* ugen (example : <code>PanAz</code>)
*/
public static final int OUTPUT_ARG = 1; // explicitly argument for the constructor
/**
* Value for <code>outputType</code> : the ugen
* has a variable number of outputs depending
* on the length of it's array argument
* (example : <code>Demand</code>)
*/
public static final int OUTPUT_ARRAYSIZE = 2; // size of a ugen input array
/**
* Name of the ugen class
*/
public final String className;
/**
* Array of the input argument definitions
*/
public final Arg[] args;
/**
* Set of all allowed rates at which the ugen can run
*
* @see Constants#kAudioRate
* @see Constants#kControlRate
*/
public final Set rates;
/**
* Maps special names (<code>String</code>s) to
* specialIndex values (<code>Integer</code>s).
* For example, the BinaryOpUGen will have mappings
* like "absdif" -> Integer( 38 ) etc.
* For UGens which do not deal with special indices,
* this field is <code>null</code>
*/
public final Map specials; // maps String special name to Integer( specialIndex ) ; may be null
/**
* Defines how the number of outputs is
* determined. One of <code>FIXED</code>, <code>ARG</code> or <code>ARRAYSIZE<code>
*/
public final int outputType; // (fixed|arg|arraySize|pre)
/**
* For fixed output ugens, the number of outputs.
* For <code>ARG</code> type ugens, the default
* number of outputs (<code>-1</code> if no defaults exist).
* For <code>ARRAYSIZE</code> type ugens, the argument
* index of the array argument to use.
*
* @warning for now, arrays are only allowed as the
* last argument of the ugen. no more than one
* array is allowed per ugen. as of september 2005,
* all known supercollider ugens fulfill this requirement
*/
public final int outputVal; // FIXED : # of outputs, ARG : default, ARRAYSIZE : arg idx
/**
* A tricky workaround invented for <code>LagControl</code> until
* i recognized that this isn't a real ugen. This could be used
* to multiply the array size for <code>ARRAYSIZE</code> output type
* ugens by a constant, say two or one half. So for now, ignore it,
* it will have the value of one for all ugens. This field
* may be deleted in one of the next versions
*/
public final float outputMul;
private UGenInfo( String className, Arg[] args, Set rates, Map specials,
int outputType, int outputVal, float outputMul )
{
this.className = className;
this.args = args;
this.rates = rates;
this.specials = specials == null ? specials : Collections.unmodifiableMap( specials );
this.outputType = outputType;
this.outputVal = outputVal;
this.outputMul = outputMul;
}
public int compareTo(Object o)
{
if( o instanceof UGenInfo ) {
return this.className.compareTo( ((UGenInfo) o).className );
} else {
throw new ClassCastException();
}
}
/**
* Returns a string ready to display
* to the user. This will return the ugens
* class name or the operator name for
* unary/binary ops
*
* @todo should find a way to present controls better
* to the user with the different control names accessible
*/
public String getDisplayName( UGen ugen )
{
if( specials == null ) return className;
final Integer specialIndex = new Integer( ugen.getSpecialIndex() );
String specialName;
for( Iterator iter = specials.keySet().iterator(); iter.hasNext(); ) {
specialName = iter.next().toString();
if( specials.get( specialName ).equals( specialIndex )) return specialName;
}
return className;
}
/**
* Returns the name for one of the ugens inputs.
* This can be used to visualize the ugen, and furthermore
* to create a keyword constructor for the ugen
* (to be done).
*/
public String getArgNameForInput( UGen ugen, int argIdx )
{
if( args.length == 0 ) return null;
if( argIdx < args.length ) {
if( args[ argIdx ].isArray ) {
return( args[ argIdx ].name + "[0]" );
} else {
return args[ argIdx ].name;
}
} else {
if( args[ args.length - 1 ].isArray ) {
return( args[ args.length - 1 ].name + '[' + (argIdx - args.length + 1) + ']' );
} else {
return null;
}
}
}
/**
* Given the number of instantiating arguments,
* returns the number of output channels the
* ugen will have. For fixed type ugens, returns
* simply the <code>outputVal</code>, for array
* type ugens, calculates the number of outputs from
* <code>numArgs</code>, for ugens that require an
* explicit number-of-channels argument, returns
* <code>pre</code> (if given) or the default value
* <code>outputVal</code>) (if specified).
*
* @param numArgs the number of arguments which will be used
* to instantiate the ugen
* @param pre pre-specified number of outputs (or <code>-1</code>)
* @return the number of output channels or <code>-1</code>
* if this method is unable to determine the number of channels
* (missing <code>pre</code> argument)
*/
public int getNumOutputs( int numArgs, int pre )
{
switch( outputType ) {
case OUTPUT_FIXED:
return outputVal;
case OUTPUT_ARRAYSIZE:
return( (int) ((numArgs - this.args.length + 1) * outputMul) );
case OUTPUT_ARG:
if( pre == -1 ) return outputVal; else return pre;
default:
assert false : outputType;
return -1;
}
}
/**
* Prints a textual representation of this
* dataset onto the given stream
*/
public void printOn( PrintStream out )
{
boolean b = false;
out.print( "UGenInfo(\"" + className + "\")\n rates: " );
for( Iterator iter = rates.iterator(); iter.hasNext(); b = true ) {
if( b ) out.print( ", " );
out.print( iter.next() );
}
out.print( "\n args: [ " );
for( int i = 0; i < args.length; i++ ) {
if( i > 0 ) out.print( ", " );
if( args[i].isArray ) out.print( "... " );
out.print( args[i].name );
if( !Float.isNaN( args[i].def )) {
out.print( " = " + args[i].def );
}
}
out.println( " ]" );
}
// public UGen ar( Object[] args )
// {
// if( !rates.contains( kAudioRate )) throw new IllegalArgumentException( kAudioRate );
// }
//
// public UGen kr( Object[] args )
// {
// if( !rates.contains( kControlRate )) throw new IllegalArgumentException( kControlRate );
// }
//
// public UGen ir( Object[] args )
// {
// if( !rates.contains( kScalarRate )) throw new IllegalArgumentException( kScalarRate );
// }
//
// public UGen dr( Object[] args )
// {
// if( !rates.contains( kDemandRate )) throw new IllegalArgumentException( kDemandRate );
// }
/**
* Reads in the ugen definition database
* from a text resource inside the libraries jar file
* (<code>ugendefs.xml</code>). Call this method
* once before using the database, i.e. before
* constructing UGens or showing a synth def diagram.
* <p>
* When this method returns, the database is available
* from the static <code>infos</code> field.
* <p>
* A much faster (around 20 times) way is to convert the xml file int
* a binary file, by calling <code>writeBinaryDefinitions</code>
* afterwards (or run JCollider with the <code>--bindefs</code> option).
* <p>
* The binary variant of this method is <code>readBinaryDefinitions</code>.
*
* @throws IOException if the definitions file couldn't be read.
* this should never happen if you don't touch the
* library. however, if you modify the xml file as to
* include new ugens, this error may occur if the xml
* file is malformed
*
* @see #infos
* @see #readBinaryDefinitions()
* @see #writeBinaryDefinitions( File )
*/
public static void readDefinitions()
throws IOException
{
//final long t1 = System.currentTimeMillis();
final Document domDoc;
final DocumentBuilderFactory builderFactory;
final DocumentBuilder builder;
final NodeList ugenList;
final Map map = new HashMap();
Element node, elem;
UGenInfo info;
try {
builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setValidating( true );
builder = builderFactory.newDocumentBuilder();
builder.setEntityResolver( dtdResolver );
domDoc = builder.parse( ClassLoader.getSystemClassLoader().getResourceAsStream( "ugendefs.xml" ));
node = domDoc.getDocumentElement();
ugenList = node.getElementsByTagName( "ugen" );
for( int i = 0; i < ugenList.getLength(); i++ ) {
elem = (Element) ugenList.item( i );
info = decodeUGenNode( domDoc, elem );
map.put( info.className, info );
if( info.specials != null ) {
for( Iterator iter = info.specials.keySet().iterator(); iter.hasNext(); ) {
map.put( iter.next(), info ); // alias entry
}
}
}
}
catch( ParserConfigurationException e1 ) {
throw new IOException( e1.getClass().getName() + " : " + e1.getLocalizedMessage() );
}
catch( SAXParseException e2 ) {
throw new IOException( e2.getClass().getName() + " : " + e2.getLocalizedMessage() );
}
catch( SAXException e3 ) {
throw new IOException( e3.getClass().getName() + " : " + e3.getLocalizedMessage() );
}
UGenInfo.infos = map;
//final long t2 = System.currentTimeMillis();
//System.out.println( "readDefinitions took " + (t2-t1) + " ms" );
}
// private static void writeP16String( RandomAccessFile raf, String s )
// {
// raf.writeShort( s.getLength() );
// raf.writeChars( str );
// }
/**
* Writes the infos out as a binary file that
* can be read in again using the <code>readBinaryDefinitions</code>
* method. You will need to move the resulting file into
* the resources folder and re-jar the library in order to
* use <code>readBinaryDefinitions</code>.
*
* @see #readDefinitions()
* @see #readBinaryDefinitions()
*/
public static void writeBinaryDefinitions( File path )
throws IOException
{
final RandomAccessFile raf;
final UGenInfo[] infos2 = new UGenInfo[ infos.size() ];
UGenInfo info;
int numInfos, iRates, flags, numSpecials;
Map.Entry me;
if( path.exists() ) {
if( !path.delete() ) throw new IOException( "Could not overwrite " + path );
}
raf = new RandomAccessFile( path, "rw" );
numInfos = 0;
for( Iterator iter = infos.entrySet().iterator(); iter.hasNext(); ) {
me = (Map.Entry) iter.next();
info = (UGenInfo) me.getValue();
if( me.getKey().equals( info.className )) {
infos2[ numInfos++ ] = info;
}
}
try {
raf.writeInt( BINARY_FILE_COOKIE );
raf.writeShort( BINARY_FILE_VERSION );
raf.writeShort( numInfos );
for( int i = 0; i < numInfos; i++ ) {
info = infos2[ i ];
iRates = 0;
if( info.rates.contains( kScalarRate )) iRates |= 0x01;
if( info.rates.contains( kControlRate )) iRates |= 0x02;
if( info.rates.contains( kAudioRate )) iRates |= 0x04;
if( info.rates.contains( kDemandRate )) iRates |= 0x08;
raf.writeUTF( info.className );
raf.writeByte( iRates );
raf.writeByte( info.outputType );
raf.writeShort( info.outputVal );
raf.writeFloat( info.outputMul );
//if( info.className.equals( "DiskOut" )) {
// System.out.println( "DiskOut: outputType = " + info.outputType + "; outputVal = " + info.outputVal + "; outputMul = " + info.outputMul );
//}
raf.writeShort( info.args.length );
for( int j = 0; j < info.args.length; j++ ) {
raf.writeUTF( info.args[ j ].name );
raf.writeFloat( info.args[ j ].min );
raf.writeFloat( info.args[ j ].max );
raf.writeFloat( info.args[ j ].def );
flags = 0;
if( info.args[ j ].isArray ) flags |= 0x01;
raf.writeByte( flags );
}
numSpecials = info.specials == null ? 0 : info.specials.size();
raf.writeShort( numSpecials );
if( numSpecials > 0 ) {
for( Iterator iter = info.specials.entrySet().iterator(); iter.hasNext(); ) {
me = (Map.Entry) iter.next();
raf.writeUTF( me.getKey().toString() );
raf.writeShort( ((Number) me.getValue()).shortValue() );
}
}
}
}
finally {
raf.close();
}
}
/**
* Reads in the ugen definition database
* from a binary resource inside the libraries jar file
* (<code>ugendefs.bin</code>). Call this method
* once before using the database, i.e. before
* constructing UGens or showing a synth def diagram.
* <p>
* When this method returns, the database is available
* from the static <code>infos</code> field.
* <p>
* To update the ugen definitions, edit the <code>ugendefs.xml</code>
* file and run JCollider with the <code>--bindefs</code> option.
* Move the resulting binary file into the <code>resources</code>
* folder and re-jar the library.
* <p>
* Reading the binary file instead of the xml file is
* a lot faster (around 20 times).
*
* @throws IOException if the definitions file couldn't be read.
*
* @see #infos
* @see #readDefinitions()
* @see #writeBinaryDefinitions( File )
*/
public static void readBinaryDefinitions()
throws IOException
{
//final long t1 = System.currentTimeMillis();
final DataInputStream dis;
final Map map;
final int numInfos;
final UGenInfo[] infos;
String className, name;
int mapSize, iRates, outputType, outputVal;
int numArgs, flags, specialValue, numSpecials;
float outputMul, min, max, def;
boolean isArray;
UGenInfo info;
Arg[] args;
Set rates;
Map specials;
dis = new DataInputStream( new BufferedInputStream( ClassLoader.getSystemClassLoader().getResourceAsStream( "ugendefs.bin" )));
try {
if( dis.readInt() != BINARY_FILE_COOKIE ) throw new IOException( "Not a valid binary ugen file" );
if( dis.readShort() > BINARY_FILE_VERSION ) throw new IOException( "Unsupport binary ugen file version" );
numInfos = dis.readShort();
infos = new UGenInfo[ numInfos ];
mapSize = numInfos;
for( int i = 0; i < numInfos; i++ ) {
className = dis.readUTF();
iRates = dis.readByte();
rates = new HashSet( 4 );
if( (iRates & 0x01) != 0 ) rates.add( kScalarRate );
if( (iRates & 0x02) != 0 ) rates.add( kControlRate );
if( (iRates & 0x04) != 0 ) rates.add( kAudioRate );
if( (iRates & 0x08) != 0 ) rates.add( kDemandRate );
outputType = dis.readByte();
outputVal = dis.readShort();
outputMul = dis.readFloat();
numArgs = dis.readShort();
args = new Arg[ numArgs ];
for( int j = 0; j < numArgs; j++ ) {
name = dis.readUTF();
min = dis.readFloat();
max = dis.readFloat();
def = dis.readFloat();
flags = dis.readByte();
isArray = (flags & 0x01) != 0;
args[ j ] = new Arg( name, min, max, def, isArray );
}
numSpecials = dis.readShort();
if( numSpecials > 0 ) {
specials = new HashMap( numSpecials );
for( int j = 0; j < numSpecials; j++ ) {
name = dis.readUTF();
specialValue = dis.readShort();
specials.put( name, new Integer( specialValue ));
}
} else {
specials = null;
}
infos[ i ] = new UGenInfo( className, args, rates, specials, outputType, outputVal, outputMul );
}
map = new HashMap( mapSize );
for( int i = 0; i < numInfos; i++ ) {
info = infos[ i ];
map.put( info.className, info );
if( info.specials != null ) {
for( Iterator iter = info.specials.keySet().iterator(); iter.hasNext(); ) {
map.put( iter.next(), info ); // alias entry
}
}
}
}
finally {
dis.close();
}
UGenInfo.infos = map;
//final long t2 = System.currentTimeMillis();
//System.out.println( "readBinaryDefinitions took " + (t2-t1) + " ms" );
}
private static UGenInfo decodeUGenNode( Document domDoc, Element node )
{
final Set rates = new HashSet();
final String className = node.getAttribute( "class" );
final NodeList argList = node.getElementsByTagName( "arg" );
final NodeList outList = node.getElementsByTagName( "outputs" );
final NodeList specialList = node.getElementsByTagName( "special" );
final Arg[] args = new Arg[ argList.getLength() ];
final Map specials;
Element elem;
String val, name;
int n, outputType, outputVal;
boolean b;
float defaultValue, outputMul;
val = node.getAttribute( "rates" );
if( val.indexOf( "audio" ) >= 0 ) rates.add( kAudioRate );
if( val.indexOf( "control" ) >= 0 ) rates.add( kControlRate );
if( val.indexOf( "scalar" ) >= 0 ) rates.add( kScalarRate );
if( val.indexOf( "demand" ) >= 0 ) rates.add( kDemandRate );
for( int i = 0; i < argList.getLength(); i++ ) {
elem = (Element) argList.item( i );
name = elem.getAttribute( "name" );
val = elem.getAttribute( "type" );
b = val.equals( "array" );
if( b && (i != argList.getLength() - 1) ) {
throw new IllegalArgumentException( className + "(arg:" + name + ") : array not allowed here" );
}
val = elem.getAttribute( "def" );
defaultValue = Float.NaN;
if( val.length() > 0 ) {
try {
defaultValue = Float.parseFloat( val );
}
catch( NumberFormatException e1 ) {
System.err.println( className + "(arg:" + name + ") : " + e1.getClass().getName() +
" : " + e1.getLocalizedMessage() );
}
}
args[ i ] = new Arg( name, defaultValue, b );
}
// #IMPLIED
outputType = OUTPUT_FIXED;
outputVal = -1;
outputMul = 1.0f;
if( outList.getLength() > 0 ) {
try {
elem = (Element) outList.item( 0 );
val = elem.getAttribute( "type" );
if( val.length() > 0 ) {
if( val.equals( "fixed" )) {
outputType = OUTPUT_FIXED;
} else if( val.equals( "arg" )) {
outputType = OUTPUT_ARG;
} else if( val.equals( "arraySize" )) {
outputType = OUTPUT_ARRAYSIZE;
}
}
val = elem.getAttribute( "val" );
if( val.length() > 0 ) {
if( outputType == OUTPUT_ARRAYSIZE ) {
for( int i = 0; i < args.length; i++ ) {
if( args[ i ].name.equals( val )) {
outputVal = i;
break;
}
}
if( outputVal == -1 ) System.err.println( className + " (outputs) : illegal ref " + val );
} else {
outputVal = Integer.parseInt( val );
}
}
val = elem.getAttribute( "mul" );
if( val.length() > 0 ) {
outputMul = Float.parseFloat( val );
}
}
catch( NumberFormatException e1 ) {
System.err.println( className + " (outputs) : " + e1.getClass().getName() +
" : " + e1.getLocalizedMessage() );
}
}
if( (outputType == OUTPUT_FIXED) && (outputVal == -1) ) outputVal = 1; // default
if( specialList.getLength() > 0 ) {
specials = new HashMap();
for( int i = 0; i < specialList.getLength(); i++ ) {
elem = (Element) specialList.item( i );
name = elem.getAttribute( "name" );
val = elem.getAttribute( "idx" );
try {
n = Integer.parseInt( val );
specials.put( name, new Integer( n ));
}
catch( NumberFormatException e1 ) {
System.err.println( className + "(arg:" + name + ") : " + e1.getClass().getName() +
" : " + e1.getLocalizedMessage() );
}
}
} else {
specials = null;
}
return new UGenInfo( className, args, rates, specials, outputType, outputVal, outputMul );
}
/**
* Descriptor for a ugen input argument.
*/
public static class Arg
{
/**
* Name of the argument (same as in SClang).
*/
public final String name;
/**
* Allowed range of its value and default value.
* The min/max fields are currently unused and
* are set to <code>Float.NEGATIVE_INFINITY</code>
* and <code>Float.POSITIVE_INFINITY</code>
* respectively. Could be used in a future version.
*/
public final float min, max, def;
/**
* If <code>true</code>, this argument requires
* an array of values. As of this version, this flag
* is only allowed once and for the last of all ugen
* input argument.
*/
public final boolean isArray;
protected Arg( String name, float min, float max, float def, boolean isArray )
{
this.name = name;
this.min = min;
this.max = max;
this.def = def;
this.isArray = isArray;
}
protected Arg( String name, float def, boolean isArray )
{
this( name, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, def, isArray );
}
// private Arg( String name, float def )
// {
// this( name, def, false );
// }
//
// private Arg( String name )
// {
// this( name, Float.NaN, false );
// }
}
private static class DTDResolver
implements EntityResolver
{
protected DTDResolver() { /* empty */ }
/**
* This Resolver can be used for loading documents.
* If the required DTD is the Meloncillo session DTD
* ("ichnogram.dtd"), it will return this DTD from
* a java resource.
*
* @param publicId ignored
* @param systemId system DTD identifier
* @return the resolved input source for
* the Meloncillo session DTD or <code>null</code>
*
* @see javax.xml.parsers.DocumentBuilder#setEntityResolver( EntityResolver )
*/
public InputSource resolveEntity( String publicId, String systemId )
throws SAXException
{
//System.err.println( "hier : "+publicId+"; "+systemId );
if( systemId.endsWith( UGENDEFS_DTD )) { // replace our dtd with java resource
InputStream dtdStream = getClass().getClassLoader().getResourceAsStream( UGENDEFS_DTD );
InputSource is = new InputSource( dtdStream );
is.setSystemId( UGENDEFS_DTD );
return is;
}
return null; // unknown DTD, use default behaviour
}
}
}