//
// GoBibleCreator.java
// GoBibleCreator
//
// Created by Jolon Faichney on Sat Oct 30 2004.
// For the glory of our Lord Jesus Christ and the furtherance of His Kingdom.
// This file is placed into the public domain.
//
import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import java.util.*;
import java.util.jar.*;
import javax.imageio.*;
import jolon.xml.*;
import java.util.zip.*;
/**
* Creates Go Bible JAR files from XML Bible texts. Supports OSIS, ThML and
* XHTML-TE formats. See OsisConverter, ThmlConverter and XhtmlTeConverter
* classes for format specific information.
*
* The collections file specifies the name of the collection, the
* contents of the collection, and the location of the source text.
*
* It has the following format:
*
*
* Source-Text: Source.thml
* Collection: Collection Name
* Book: Book Name, Start Chapter, End Chapter
* Book: Book Name, Start Chapter, End Chapter
* Collection: Collection Name
* Book: Book Name, Start Chapter, End Chapter
*
* The book name is that which is contained within the div2 title tag.
* The start and end chapters can be omitted if the entire book is to be
* included.
* An example would be:
*
* Source-Text: KJV.thml
* Collection: Gospels
* Book: Matthew
* Book: Mark
* Book: Luke
* Book: John
* Collection: Psalms I
* Book: Psalms, 1, 41
* Collection: Psalms II
* Book: Psalms, 42, 72
*
*
**/
public abstract class GoBibleCreator
{
/**
* Version of Go Bible to be written to JAR and JAD files. Major version will
* be the MIDP version. eg. MIDP 2.0 version will be 2.x.x.
**/
public final static String SUB_VERSION = "4.99"; // "3.6";
/** Style changes are written out as flags in a single byte. **/
public final static char STYLE_RED = 1;
/** Text that will be appended to every collection name. **/
public final static String NAME_APPENDAGE = " Go Bible";
/** Combines multiple chapters into a single file. File size will be approximately MAX_FILE_SIZE. **/
public final static boolean COMBINED_CHAPTERS = true;
/** If COMBINED_CHAPTERS = true then this represents the approximate file size of the combined chapters. **/
public final static int MAX_FILE_SIZE_MIDP_2 = 24 << 10;
public final static int MAX_FILE_SIZE_MIDP_1 = 4 << 10;
/** Go Bible text alignment: LEFT. **/
public final static int ALIGN_LEFT = 0;
/** Go Bible text alignment: RIGHT. **/
public final static int ALIGN_RIGHT = 1;
/** GoBible-Align JAD property values. **/
public final static String[] ALIGN_TEXT = {"Left", "Right"};
/** File name of the UI properties file. **/
public final static String UI_PROPERTIES_FILE_NAME = "ui.properties";
/** MIDP version in the manifest and JAD. **/
public static String midpVersion = "MIDP-2.0";
//the USFM file codepage for the source files
// introduced in version 2.3
protected static String fileCodepage = null;
/** MIDP version in the manifest and JAD. **/
public static String versionString = "2." + SUB_VERSION;
public static int MAX_FILE_SIZE = MAX_FILE_SIZE_MIDP_2;
/**
* By default GoBibleCreator parses the source text and generates the verse
* data which is stored in the JAR files. This is a time consuming process
* and rarely needs to be repeated. If "-u" is specifed on the command-line
* then this variable is set to true which means to only update the existing
* JAR/JAD files instead of reparsing the source text and generating the
* verse data. However, it is off by default to ensure that the files are
* generated.
*/
protected static boolean updateOnly = false;
/** Location of WAP site where JAD files will be placed.
If no 'Wap-site:' attribute is specified in the Collections file then no WAP files will be
produced. **/
protected static String wapSite = null;
/* attributes deleted: (put them elsewhere)
* MIDlet-INfo_URL
* MIDlet-Vendor
* Info-string
*/
/** Custom font string. **/
protected static String customFontString = null;
/** Text alignment. **/
protected static int align = ALIGN_LEFT;
/** Language Code appended to Collection names. eg. Ar, Zh, En, etc. **/
protected static String languageCode = "";
public enum languageCodePositionType
{
prefix,
suffix,
};
protected static languageCodePositionType languageCodePosition;
/** Contains the contents of the English UI properties file and
non-English UI strings specified in the Collections file.
**/
protected static Properties uiProperties = new Properties();
/**
* This will be prepended to the Source-Text path in the Collections.txt file.
* Can be set through the -d parameter.
*/
protected static String baseSourceDirectory = null;
// the file path (relative to the baseSource directory) to an alternate
// icon that will display on the phone
// introduced in version 2.3
protected static String phoneIconFilepath = null;
// alternate name for the application that will be displayed in the
// phone's title
// introduced in version 2.3
protected static String applicationName = null;
// flag to determine if red lettering is used
// introduced in version 2.3
protected static boolean useRedLettering = true; /* TODO: duplicate */
public enum SourceFormatType
{
osis,
thml,
usfm,
xhtml_te,
unknown
};
// introduced in version 2.3
protected static SourceFormatType sourceFormatType;
/** Empty-Verse-Text string. **/
protected static String EmptyVerseString = null;
private static final int MODE_GENERATE_COLLECTION = 0;
private static final int MODE_CREATE_JAR = 1;
private static final int MODE_UPDATE_BINARIES = 2;
private static String fileName = null;
private static int actionMode = MODE_CREATE_JAR;
private static void printHeader() {
System.out.println("----------------------------------");
System.out.println("GoBibleCreator Version: " + versionString);
System.out.println("----------------------------------");
}
private static void printUsage() {
System.out.println("Usage: (java -jar GoBibleCreator) [ options ] CollectionsFilePath");
System.out.println("\tOptions:");
System.out.println("\t-b (path) - Path to GoBibleCore binaries (.jar)");
System.out.println("\t-m (path) - Path to Manifest file template");
System.out.println("\t-p (path) - Path to ui.properties template");
System.out.println("\t-dh - Show some debug output");
System.out.println("\t-u - Update only");
System.out.println("\t-d - Set base source text directory");
}
private static void readProgramOptions(String args[]) {
printHeader();
if (args.length < 1)
{
printUsage();
System.exit(0);
}
String paramOptionMap[][] = {
{ "-b", "GBCoreBinaryPath" },
{ "-m", "ManifestFilePath" },
{ "-p", "UIFilePath" },
{ "-dh", null},
{ "-u", null},
{ "-d", "BaseSourceDirectory" }
};
String updateOption = null;
int i;
for (i=0; i " + System.getProperty(property));
}
}
catch (Exception e) {}
System.out.println("***************************************************");
System.out.println();
}
if (programOptions.containsKey("-u"))
{
// Update-only flag has been specified which means we won't be
// parsing the source text or updating the text in the JAR file.
// Instead we will be only modifying the existing JAR/JAD files
// which should already exist
updateOnly = true;
System.out.println(" ** The \"Update-only\"(-u) flag has been found: updating the JAR/JAD files.");
System.out.println(" ** The original JAR files[s] are renamed with the file extension .TMP");
}
if (programOptions.containsKey("BaseSourceDirectory"))
{
// This argument is the base directory for Source-Text, we can
// switch the flag off now and considered the other arguments
// as normal.
baseSourceDirectory = programOptions.get("BaseSourceDirectory");
System.out.println(" ** Using <"+baseSourceDirectory+"> as the base Source Directory.");
}
System.out.println(" ** Using the GoBibleCore binary:\n\t<" + programOptions.get("GBCoreBinaryPath") + ">");
System.out.println(" ** Using the Manifest file:\n\t<" + programOptions.get("ManifestFilePath") + ">");
System.out.println(" ** Using the UI Properties file:\n\t<" + programOptions.get("UIFilePath") + ">");
for (; i programOptions = null;
private static HashMap goBibleOptions = new HashMap();
public static void initializeOptions() {
File jarDirectory = getJarDirectory();
if (programOptions == null) {
programOptions = new HashMap();
}
programOptions.put("ManifestFilePath", new File(jarDirectory, "./GoBibleCore/MANIFEST.MF").getAbsolutePath());
programOptions.put("GBCoreBinaryPath", new File(jarDirectory, "./GoBibleCore/GoBibleCore.jar").getAbsolutePath());
programOptions.put("UIFilePath", new File(jarDirectory, "./GoBibleCore/ui.properties").getAbsolutePath());
System.out.println(programOptions.get("UIFilePath"));
if (goBibleOptions == null) {
goBibleOptions = new HashMap();
}
goBibleOptions.put("Go-Bible-Align", "left");
}
/**
* Parses the XML and collection files and writes out the Go Bible data
* files in the same directory as the collection files.
* @param collectionsFile Collections File
*/
public static void create(File collectionsFile) throws IOException
{
HashMap books = null;
if (!updateOnly)
{
// Extract xml file from the collectionsFile property: Source-Text
String sourceTextPath = extractSourceTextPath(collectionsFile);
// Extract the source file type from the collectionsFile property: SourceFormat
sourceFormatType = extractSourceFormatType(collectionsFile);
if (sourceFormatType.equals(SourceFormatType.unknown))
{
//do extra testing on the file to see if the format can be determined
//this is for backward compatibility for GBC 2.2.6 version and prior
//which did not specify the Source-Format property
sourceFormatType = SourceFormatTypeRetry(baseSourceDirectory, sourceTextPath);
}
// Base source directory can be overridden with the -d argument
UpdateBaseSourceDirectory(collectionsFile);
if (sourceFormatType == SourceFormatType.osis
|| sourceFormatType == SourceFormatType.thml
|| sourceFormatType == SourceFormatType.xhtml_te)
{
File xmlFile = new File(baseSourceDirectory, sourceTextPath);
XMLConverter xc = XMLConverter.getConverter(xmlFile);
books = xc.parse();
}
else if (sourceFormatType == SourceFormatType.usfm)
{
USFMConverter uc = new USFMConverter
(collectionsFile,
baseSourceDirectory,
sourceTextPath);
books = uc.parse();
}
else
{
//dump out message saying that it cannot determine the file format type
System.out.println("Error: Could not determine Bible format type, " +
sourceFormatType + ". Please use the 'Source-Format'");
System.out.println("property in your collections file.");
return;
}
}
else // updateOnly
{
if (actionMode == MODE_CREATE_JAR) {
UpdateBaseSourceDirectory(collectionsFile); // to update icons
}
}
if (books != null || updateOnly)
{
Vector collections = null;
if (actionMode == MODE_CREATE_JAR) {
/* TODO: Collections file is parsed AFTER files are interpreted?
* HOw does it affect the fileCodePage?
*/
// Parse the collections file
collections = parseCollectionsFile(collectionsFile, books);
if (customFontString != null)
{
generateCustomFont(customFontString, collectionsFile, books);
}
}
// Work out the current directory of the GoBibleCreator.jar
File jarFile = new File(programOptions.get("GBCoreBinaryPath"));
JarFile goBibleJar = new JarFile(jarFile);
// Find the manifest file attributes and load it.
File manifestFile = new File(programOptions.get("ManifestFilePath"));
MyManifest manifest;
try {
System.out.println(manifestFile.getCanonicalFile().getAbsolutePath());
manifest = new MyManifest(new FileInputStream(manifestFile));
System.out.println("Manifest template found");
}
catch (IOException ioe) {
ioe.printStackTrace(System.out);
manifest = new MyManifest();
System.out.println("Manifest data NOT found");
}
for (Enumeration e = collections.elements(); e.hasMoreElements(); )
{
Collection collection = (Collection) e.nextElement();
System.out.println("Writing Collection " + collection.fileName + ": ");
writeCollection(
collectionsFile.getParentFile(),
collection,
books,
goBibleJar,
manifest);
}
}
}
/**
* @return The directory containing the GoBibleCreator.jar file that is being executed.
*/
public static File getJarDirectory()
{
String classPath = System.getProperty("java.class.path");
String pathSeparator = System.getProperty("path.separator");
String[] paths = classPath.split(pathSeparator);
// Find the path which contains GoBibleCreator.jar
for (String path: paths)
{
if (path.contains("GoBibleCreator.jar"))
{
File file = new File(path);
return file.getParentFile();
}
}
System.out.println("Warning: couldn't find the path to GoBibleCreator.jar. Using current directory.");
return new File(".");
}
public static String extractSourceTextPath(File collectionsFile) throws IOException
{
String sourceTextPath = null;
// Open the file for reading one line at a time in UTF-8 character encoding
LineNumberReader reader = new LineNumberReader(new InputStreamReader(new FileInputStream(collectionsFile), "UTF-8"));
String line = null;
String sourceTextPropertyName = "Source-Text:";
// Read the collections in the file
while ((line = reader.readLine()) != null)
{
// Test if line contains the source text property
if (line.startsWith(sourceTextPropertyName))
{
sourceTextPath = line.substring(sourceTextPropertyName.length()).trim();
break;
}
}
reader.close();
if (sourceTextPath == null)
{
System.err.println("Error parsing collections file: No Source-Text specified.");
System.err.println("The source text is either a ThML, OSIS or XHTML-TE XML file.");
System.err.println("It must be specified as follows:");
System.err.println("Source-Text: Source.xml");
System.err.println("For example:");
System.err.println("Source-Text: kjv.thml");
}
return sourceTextPath;
}
/*
* @return the enum of the source file format (i.e., Thml/osis/usfm)
*/
public static SourceFormatType extractSourceFormatType(File collectionsFile) throws IOException
{
SourceFormatType srcFormatType = SourceFormatType.unknown;
// Open the file for reading one line at a time in UTF-8 character encoding
LineNumberReader reader = new LineNumberReader(new InputStreamReader(new FileInputStream(collectionsFile), "UTF-8"));
String line = null;
String sourceTextPropertyName = "Source-Format:";
// Read the collections in the file
while ((line = reader.readLine()) != null)
{
// Test if line contains the source text property
if (line.startsWith(sourceTextPropertyName))
{
String sFormat = line.substring(sourceTextPropertyName.length()).trim().toLowerCase();
if (sFormat.equals("osis"))
{ srcFormatType = SourceFormatType.osis; }
else if (sFormat.equals("thml"))
{ srcFormatType = SourceFormatType.thml; }
else if (sFormat.equals("usfm"))
{ srcFormatType = SourceFormatType.usfm; }
else if (sFormat.equals("xhtml_te"))
{ srcFormatType = SourceFormatType.xhtml_te; }
else
{ srcFormatType = SourceFormatType.unknown; }
break;
}
}
reader.close();
return srcFormatType;
}
/*
* tries one last time to parse the source file to determine the file format
*/
public static SourceFormatType SourceFormatTypeRetry(String baseSourceDirectory, String sourceTextPath) throws IOException
{
SourceFormatType srcFormatType = SourceFormatType.unknown;
try
{
//convert source text to file
File fIn = new File(baseSourceDirectory, sourceTextPath);
//read in up to the first 200 lines and see if you can spot the tags
BufferedReader readerIn = new BufferedReader(new FileReader(fIn));
String lineIn;
int iRowCount = 0;
while ((lineIn = readerIn.readLine()) != null || iRowCount < 200)
{
lineIn = lineIn.toLowerCase();
if (lineIn.indexOf(OsisConverter.OSIS_TAG.toLowerCase()) > 0)
{
srcFormatType = SourceFormatType.osis;
break;
}
if (lineIn.indexOf(ThmlConverter.THML_TAG.toLowerCase()) > 0)
{
srcFormatType= SourceFormatType.thml;
break;
}
if (lineIn.indexOf(XhtmlTeConverter.XHTML_TAG.toLowerCase()) > 0)
{
srcFormatType= SourceFormatType.xhtml_te;
break;
}
iRowCount++;
}
readerIn.close();
}
catch(Exception e)
{
System.out.println("Error: " + e.getMessage());
e.printStackTrace();
}
return srcFormatType;
}
/**
* Parses the English UI properties file. The English definitions
* can be overridden in the Collections file.
*/
public static void parseUiProperties(InputStream stream) throws IOException
{
// Open the file for reading one line at a time in UTF-8 character encoding
LineNumberReader reader = new LineNumberReader(new InputStreamReader(stream, "UTF-8"));
String line = null;
// Read the properties in the file
while ((line = reader.readLine()) != null)
{
if (line.startsWith("UI-"))
{
int index = line.indexOf(':');
uiProperties.put(line.substring(0, index), line.substring(index + 1));
}
else if(line.startsWith("#") ||line.startsWith("//") || line.startsWith("rem") || line.startsWith("REM"))
{
//do nothing - ignore line
}
// If line isn't empty then report that we don't know what it is
else if (!line.trim().equals(""))
{
System.out.println("Error parsing ui.properties file. Can't understand line:\n" + line);
}
}
// Close the file
reader.close();
}
/**
* Creates a vector of collections from the specified file.
* @param collectionsFile Collections file with the format specified above.
* @param books HashMap of books indexed by book name.
* @return Vector of Collection objects.
**/
public static Vector parseCollectionsFile(File collectionsFile, HashMap books) throws IOException
{
// Open the file for reading one line at a time in UTF-8 character encoding
LineNumberReader reader = new LineNumberReader(new InputStreamReader(new FileInputStream(collectionsFile), "UTF-8"));
Vector collections = new Vector();
Collection collection = null;
String line = null;
int lineCount = 0;
java.util.regex.Matcher m = null;
// Read the collections in the file
while ((line = reader.readLine()) != null)
{
lineCount++;
//ignore blank lines
if (!line.trim().equals(""))
{
//test to see if the line is a comment line
if (line.startsWith("//") || line.startsWith("rem") || line.startsWith("REM"))
{
//do nothing - ignore line
}
// Test if line specifies a WAP site for the JAD files
else if (line.startsWith("Wap-site:"))
{
wapSite = line.substring(9).trim();
}
// Source-Text is ignored here but is retrieved earlier from within extractSourceTextPath()
else if (line.startsWith("Source-Text:"))
{
}
// Source-FileExtension is the extension of the USFM files in the format ptx,ltn,uz..
// retrieved earlier
else if (line.startsWith("Source-FileExtension:"))
{
}
// Source-Format is ignored here but is retrieved earlier
// Added in version > 2.2.6
else if (line.startsWith("Source-Format:"))
{
}
// USFM-TitleTag is ignored here but is retrieved earlier
// Added in version > 2.2.6
else if (line.startsWith("USFM-TitleTag:"))
{
}
// Phone-Icon-Filepath is the filepath to the alternate icon
// to be displayed by the application
// Added in version > 2.2.6
else if (line.startsWith("Phone-Icon-Filepath:"))
{
phoneIconFilepath = line.substring("Phone-Icon-Filepath:".length()).trim();
}
else if (line.startsWith("Codepage:"))
{
fileCodepage = line.substring("Codepage:".length()).trim();
}
// alternate name for the application that will be displayed in the
// phone title
// introduced in version > 2.2.6
else if (line.startsWith("Application-Name:"))
{
applicationName = line.substring("Application-Name:".length()).trim();
}
// Check to see if the user wants to use red lettering
// brought in earlier
else if (line.startsWith("RedLettering:"))
{
//defaults to true
if (line.toLowerCase().indexOf("false") > 0 || line.toLowerCase().indexOf("no") > 0)
{
useRedLettering = false;
}
}
// Test if line specifies Info property
else if (line.startsWith("Info:"))
{
goBibleOptions.put("Go-Bible-Info", line.substring(5).trim());
}
// Test if line specifies Custom-Font property
else if (line.startsWith("Custom-Font:"))
{
customFontString = line.substring(12).trim();
}
// Test if line specifies Language-Code property
else if (line.startsWith("Language-Code:"))
{
int commaIndex = line.indexOf(',');
if (commaIndex == -1)
{
languageCode = line.substring(14).trim();
languageCodePosition = languageCodePositionType.suffix; // default value
}
else
{
languageCode = line.substring(14, commaIndex);
String langPosition = line.substring(commaIndex + 1).trim();
if (langPosition.equalsIgnoreCase("prefix"))
{
languageCodePosition = languageCodePositionType.prefix;
}
else if (langPosition.equalsIgnoreCase("suffix"))
{
languageCodePosition = languageCodePositionType.suffix;
}
else
{
languageCodePosition = languageCodePositionType.suffix; // default value
System.out.println("Error parsing collections file. Unsupported Language-Code position: " + langPosition + ", use 'prefix' or 'suffix'. (EX: Language-Code: en, prefix)");
}
}
}
// Test if line specifies MIDP property
else if (line.startsWith("MIDP:"))
{
String midpVersionString = line.substring(5).trim();
// Release 2.4.0 will no longer support MIDP-1.0
//if (midpVersionString.equals("MIDP-1.0"))
//{
// midpVersion = midpVersionString;
// MAX_FILE_SIZE = MAX_FILE_SIZE_MIDP_1;
//}
//else
if (!midpVersionString.equals(midpVersion)) //"MIDP-2.0"))
{
// System.out.println("Error parsing collections file. Unsupported MIDP version: " + midpVersionString + ", try MIDP-1.0 or MIDP-2.0");
System.out.println("Error parsing collections file. Unsupported MIDP version: " + midpVersionString + ", use " + midpVersion);//MIDP-2.0");
}
}
// Test if this is the alignment property
else if (line.startsWith("Align:"))
{
// Set the alignment
String alignString = line.substring(6).trim().toLowerCase();
if (alignString.equals("left"))
{
goBibleOptions.put("Go-Bible-Align", alignString);
}
else if (alignString.equals("right"))
{
goBibleOptions.put("Go-Bible-Align", alignString);
}
else
{
System.out.println("Error passing collections file. Did not understand align property: '" + alignString + "'. Must be either 'Left' or 'Right'.");
}
}
// Test if this is a book name map
else if (line.startsWith("Book-Name-Map:"))
{
int length = 14;
// Grab the book short name
String valuePart = line.substring(length).trim(); // trim away initial tabs
String parts[] = valuePart.split("[,\\t]",2);
String bookLongName = parts[1].trim();
String bookShortName = parts[0].trim();
if (!updateOnly)
{
// Find the book
Book book = (Book) books.get(bookShortName);
if (book == null)
{
System.out.println("Warning: can't find book name: " + bookShortName);
System.out.println("Check Book-Name-Map entries with source text file[s].");
}
else
{
// Set the book's long name
book.name = bookLongName;
}
}
}
// For files with \id, use Book-Name, only tabs allowed
else if (line.startsWith("Book-Index-Id-Name:"))
{
char delimiter = '\t';
String value = line.substring(5+6+3+5);
String parts[] = value.split("/\t/",3);
parts[0] = parts[0].trim();
parts[1] = parts[1].trim();
parts[2] = parts[2].trim();
if (!updateOnly)
{
// Find the book
Book book = (Book) books.get(parts[1]);
if (book == null)
{
System.out.println("Warning: can't find book id: " + parts[1]);
System.out.println("Check Book-Index-Id-Name entries with source text file[s].");
}
else
{
// Set the book's long name
book.name = parts[2];
}
}
}
// Test if this is a new collection
else if (line.startsWith("Collection:"))
{
String collectionName = line.substring(11).trim();
String fileName = collectionName;
// If there is a comma then the first name is the file name
// and the second name is the collection name
int commaIndex = collectionName.indexOf(',');
if (commaIndex != -1)
{
fileName = collectionName.substring(0, commaIndex);
collectionName = collectionName.substring(commaIndex + 1).trim();
}
// Create a new collection with the name after "Collection:"
if (languageCodePosition == languageCodePositionType.suffix)
{
collection = new Collection(fileName + " " + languageCode, (collectionName + " " + languageCode).trim());
}
else
{
collection = new Collection(languageCode + " " + fileName, (languageCode + " " + collectionName).trim());
}
collections.add(collection);
}
else if (line.startsWith("Book:") ||
(m = (java.util.regex.Pattern.compile("^Book-([0-9]+):")).matcher(line)).matches())
{
int bookNumber = -1;
if (m != null && m.matches()) {
bookNumber = Integer.parseInt(m.group(1));
}
// If an existing collection doesn't exist then it is an error
if (collection == null)
{
System.out.println("'Book:' without collection on line " + reader.getLineNumber());
}
// The book line may contain commas if start and end chapters are specified
String bookName = line.substring(5);
int startChapter = -1;
int endChapter = -1;
int commaIndex = bookName.indexOf(',');
if (commaIndex >= 0)
{
// Grab the chapter numbers after the comma
String startChapterString = bookName.substring(commaIndex + 1);
// Remove the commas and chapter numbers from the book name
bookName = bookName.substring(0, commaIndex);
// Grab the second comma
commaIndex = startChapterString.indexOf(',');
if (commaIndex >= 0)
{
String endChapterString = startChapterString.substring(commaIndex + 1).trim();
startChapterString = startChapterString.substring(0, commaIndex).trim();
startChapter = Integer.parseInt(startChapterString);
endChapter = Integer.parseInt(endChapterString);
}
else
{
System.out.println("Start chapter specified without end chapter on line " + reader.getLineNumber());
}
}
// Trim whitespace from around the book name
bookName = bookName.trim();
//clean up the extranous characters in the book name
if(sourceFormatType == SourceFormatType.usfm)
{
bookName = USFMConverter.CleanBookName(bookName);
}
// If the start and end chapters aren't specified then get them from the XML book.
// We can only do this if updateOnly isn't set, otherwise the source text
// won't have been parsed.
if (startChapter == -1 && !updateOnly)
{
Book xmlBook = (Book) books.get(bookName);
if (xmlBook == null)
{
System.out.println("Couldn't find book: " + bookName);
}
startChapter = xmlBook.startChapter;
endChapter = xmlBook.chapters.size() + startChapter - 1;
}
// Create a new book and add it to the current collection
Book book = new Book(bookName, startChapter, endChapter);
book.bookNumber = bookNumber;
collection.add(book);
}
else if (line.startsWith("UI-"))
{
// Override English definition
int index = line.indexOf(':');
uiProperties.put(line.substring(0, index), line.substring(index + 1));
}
// Empth verse text
else if (line.startsWith("Empty-Verse-Text:"))
{
// Grab text to display where the verse text is missing
EmptyVerseString = line.substring(15).trim();
}
// Any other random property
else if (line.startsWith("Go-Bible-") || line.startsWith("MIDlet-"))
{
// Set the alignment
int colonPosition = line.indexOf(":");
if (colonPosition == -1) {
System.out.println("Error parsing collections file. Can't understand line<"+lineCount+">:\n" + line);
}
String propertyString = line.substring(0, colonPosition);
String valueString = line.substring(colonPosition + 1).trim();
goBibleOptions.put(propertyString, valueString);
}
// If line isn't empty then report that we don't know what it is
else if (!line.trim().equals(""))
{
System.out.println("Error parsing collections file. Can't understand line<"+lineCount+">:\n" + line);
}
}
} // While
// Close the file
reader.close();
return collections;
}
/**
* Creates a new directory within the specified directory and writes out the specified
* collection therein. Also creates a wap subdirectory where WAP JAD files are placed. Adds a new line
* to the wapPage StringBuffer creating a link to the WAP JAD file.
* @param directory Directory to place the collection in.
* @param collection Collection to create.
* @param books HashMap of books read in from XML file.
* @param goBibleJar JAR containing the Go Bible classes.
* @param wapPage StringBuffer containing the contents of the WAP page where a new line will be added for
* the current collection.
**/
public static void writeCollection(
File directory,
Collection collection,
HashMap books,
JarFile goBibleJar,
Manifest templateManifest) throws IOException
{
// Create a manifest file for the new JAR
MyManifest finalManifest = new MyManifest();
HashMap versionOptions = new HashMap();
// prepare the name for this application
String s = NAME_APPENDAGE;
if (applicationName != null) {
s = applicationName;
}
// Default options that prevent a program from crashing if Manifest file is not found
// But don't overwrite a previous GoBible name.
if (collection != null && actionMode != MODE_UPDATE_BINARIES) {
versionOptions.put("MIDlet-Name", collection.name + " " + s);
versionOptions.put("MIDlet-1", collection.name + " " + s + ", /Icon.png, GoBible");
}
versionOptions.put("MIDlet-Icon", "/Icon.png");
versionOptions.put("MIDlet-Version", "2.4.99"); /* some dummy version number */
versionOptions.put("MIDlet-Data-Size", "100");
versionOptions.put("MicroEdition-Configuration", "CLDC-1.0");
versionOptions.put("MicroEdition-Profile", "MIDP-2.0");
finalManifest.importData(templateManifest.getMainAttributes());
finalManifest.importData(versionOptions);
finalManifest.importData(goBibleOptions);
Attributes attributes = finalManifest.getMainAttributes();
// Create a String to contain the UI properties
String uiPropertiesString = "";
Set uiMappings = uiProperties.entrySet();
for (Iterator i = uiMappings.iterator(); i.hasNext(); )
{
Map.Entry entry = (Map.Entry) i.next();
uiPropertiesString += entry.getKey() + ":" + entry.getValue() + "\n";
}
if (actionMode == MODE_UPDATE_BINARIES) {
updateCollectionJar(
directory.getParentFile(),
directory,
goBibleJar,
finalManifest,
uiPropertiesString);
createJadFile(
directory.getParentFile(),
new File(
directory.getParentFile(),
directory.getName().replaceAll(".jar$", ".jad")), // TODO if .jar exists inside the name this fails
directory.length(),
"http://example.com/" + directory.getName(),
attributes);
return;
}
else {
// Create a new JAR file using the contents of the Go Bible JAR and the new manifest
File jarFile = new File(directory, collection.fileName + ".jar");
File jadFile = new File(directory, collection.fileName + ".jad");
// If updateOnly is true then we haven't parsed the source text and hence
// won't be generating any books or verse data so we are going to
// source the Bible Data from the existing JAR file
if (updateOnly)
{
updateCollectionJar(directory, jarFile, goBibleJar, finalManifest, uiPropertiesString);
}
else
{
writeCollectionJar(directory, jarFile, collection, books, goBibleJar, finalManifest, uiPropertiesString);
}
// Create the JAD file that will go in the zip
createJadFile(directory, jadFile, jarFile.length(), collection.fileName + ".jar", attributes);
if (wapSite != null)
{
File wapDirectory = new File(directory, "wap");
if (!wapDirectory.exists())
{
wapDirectory.mkdir();
}
// Create the JAD file for the WAP site
createJadFile(
wapDirectory,
new File(wapDirectory, collection.fileName + ".jad"),
jarFile.length(),
wapSite + "/" + directory.getName() + "/" + collection.fileName + ".jar",
attributes);
// Add a line to the wapPage
/*wapPage.append("\n");
wapPage.append("" + collection.name + " " + (jarFile.length() >> 10) + "k \n\n");*/
}
}
}
/**
* 1. Rename existing jar file appending ".tmp"
* 2. Create new jar file with initial name
* 3. Copy "Bible Data" entry of tmp jar file to new jar file
* 4. Delete tmp jar file.
*/
public static void updateCollectionJar(File directory, File jarFile, JarFile goBibleJar, Manifest manifest, String uiPropertiesString) throws IOException
{
// Make sure the existing JAR exists
if (jarFile.exists())
{
File tmpFile = File.createTempFile("gobible", ".jar.tmp", directory);
String oldName = jarFile.getName();
jarFile.renameTo(tmpFile);
// Recreate the JAR file using the tmp as a base
jarFile = new File(directory, oldName);
System.err.println(directory);
System.err.println(tmpFile);
JarFile tmpJar = new JarFile(tmpFile);
JarOutputStream jarOutputStream = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(jarFile)), manifest);
// Copy in the Bible Data from the original JAR
copyInContentsOfJar(tmpJar, jarOutputStream, "Bible Data");
// Copy in GoBibleCore
copyInContentsOfJar(goBibleJar, jarOutputStream, null);
// Create a JAR entry for the UI properties
jarOutputStream.putNextEntry(new JarEntry(UI_PROPERTIES_FILE_NAME));
jarOutputStream.write(uiPropertiesString.getBytes("UTF-8"));
jarOutputStream.close();
// Remove the tmp file
tmpFile.delete();
}
else
{
System.out.println("Error: existing JAR file does not exist: " + jarFile.getAbsolutePath());
System.out.println("An existing JAR file must exist when the -u option is used.");
}
}
/**
* Creates a JAR file containing all of the Books specified in the Collections.txt file.
*/
public static void writeCollectionJar(File directory, File jarFile, Collection collection, HashMap books, JarFile goBibleJar, Manifest manifest, String uiPropertiesString) throws IOException
{
JarOutputStream jarOutputStream = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(jarFile)), manifest);
// Copy from Go Bible Jar
copyInContentsOfJar(goBibleJar, jarOutputStream, null);
// Create a JAR entry for the UI properties
jarOutputStream.putNextEntry(new JarEntry(UI_PROPERTIES_FILE_NAME));
jarOutputStream.write(uiPropertiesString.getBytes("UTF-8"));
writeMultipleIndex(jarOutputStream, collection, books);
writeMultipleMappings(jarOutputStream, collection, books);
writeMultipleBooks(jarOutputStream, collection, books);
jarOutputStream.close();
System.out.println(collection.books.size() + " book(s) written to " + jarFile);
}
/**
* Copies the contents of one JAR into another already open JAR.
* @param jar The source JAR to copy from.
* @param jarOutputStream The destination JAR to copy to.
* @param name Will only copy entries that begin with this name, can be null to copy everything.
*/
public static void copyInContentsOfJar(JarFile jar, JarOutputStream jarOutputStream, String name) throws IOException
{
// Copy contents of Go Bible JAR into new JAR
for (Enumeration e = jar.entries(); e.hasMoreElements(); )
{
JarEntry jarEntry = (JarEntry) e.nextElement();
//System.out.println("Reading entry from GoBible.jar: " + jarEntry.getName());
String entryName = jarEntry.getName();
String sFilepath = "";
// Ignore existing manifest, and ui.properties file if
// the Collections file has specified UI properties
if (!entryName.startsWith("META-INF") &&
!entryName.equals(UI_PROPERTIES_FILE_NAME) &&
(name == null || entryName.startsWith(name)))
{
boolean bNewIcon = false;
if (entryName.equals("/Icon.png") && phoneIconFilepath != null)
{
//check for file existance first
File oFile = new File(baseSourceDirectory, phoneIconFilepath);
if (oFile.exists())
{
bNewIcon = true;
sFilepath = oFile.getPath();
}
else
System.out.println("Error: Icon file doesn't exist <"+phoneIconFilepath+">.");
}
InputStream inputStream;
if (!bNewIcon)
{
// Add entry to new JAR file
jarOutputStream.putNextEntry(jarEntry);
//copy over the resource from the jar
inputStream = new BufferedInputStream(jar.getInputStream(jarEntry));
// Read all of the bytes from the Go Bible JAR file and write them to the new JAR file
byte[] buffer = new byte [100000];
int length;
while ((length = inputStream.read(buffer)) != -1)
{
jarOutputStream.write(buffer, 0, length);
}
// Close the input stream
inputStream.close();
}
else
{
//add in the replacement icon
byte[] buffer = new byte[100000];
FileInputStream fi = new FileInputStream(sFilepath);
inputStream = new BufferedInputStream(fi, 100000) ;
ZipEntry entry = new ZipEntry("Icon.png");
jarOutputStream.putNextEntry ( entry ) ;
int count;
while ((count=inputStream.read(buffer, 0, 100000)) != -1 )
{
jarOutputStream.write ( buffer, 0, count ) ;
}
inputStream.close();
}
}
}
}
/**
* Creates a JAD file in the specified location, for the specified collection, with the specified JAR file length,
* with the specified URL.
* @param directory Location to place the JAD file.
* @param collection Collection for which the JAD file is being created for.
* @param jarFileLength Length in bytes of the JAR file.
* @param url The string to place in the MIDlet-Jar-URL property.
* @param attr Re-use the attributes found in the Manifest.
**/
public static void createJadFile
(File directory,
File jadFile,
long jarFileLength,
String url,
Attributes attr)
throws IOException
{
System.err.println(jadFile.getAbsolutePath());
PrintWriter writer = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(jadFile), "UTF-8")));
// retrieve and write values
Set s = attr.keySet();
// sort the values for presentation
TreeSet sorted = new TreeSet( new Comparator() {
public int compare(Attributes.Name a, Attributes.Name b) {
return a.toString().compareTo(b.toString());
}
});
sorted.addAll(s);
for (Object key: sorted) {
writer.print(key.toString());
writer.print(": ");
writer.print(attr.getValue(key.toString()));
writer.println();
}
writer.println("MIDlet-Jar-Size: " + jarFileLength);
writer.println("MIDlet-Jar-URL: " + url);
writer.close();
}
/**
* Writes the index of the collection.
* @param directory Directory to place index.
* @param collection Collection to create.
* @param books Books parsed from ThML file.
**/
public static void writeMultipleIndex(/*File directory,*/ JarOutputStream jarOutputStream, Collection collection, HashMap books) throws IOException
{
// Write out the main bible index
// DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(new File(directory, "Index"))));
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(byteArrayOutputStream);
// Write out the number of books
output.write(collection.books.size());
// Write the number of chapters and verses for each book
for (Enumeration e = collection.books.elements(); e.hasMoreElements(); )
{
Book collectionBook = (Book) e.nextElement();
Book thmlBook = (Book) books.get(collectionBook.name);
// Write the book name
output.writeUTF(thmlBook.name);
// Write the book's file name
output.writeUTF(thmlBook.fileName);
// Write the start chapter and number of chapters
output.writeShort(collectionBook.startChapter);
output.writeShort(collectionBook.endChapter - collectionBook.startChapter + 1);
int fileNumber = 0;
int fileLength = 0;
for (int i = collectionBook.startChapter; i <= collectionBook.endChapter; i++ )
{
Chapter chapter = (Chapter) thmlBook.chapters.elementAt(i - thmlBook.startChapter);
if (COMBINED_CHAPTERS)
{
// If this isn't the first chapter for the file and the length of the
// next chapter will be greater than the maximum allowed file length
// then put this chapter into the next file
if ((fileLength != 0) && ((fileLength + chapter.allVerses.length() - MAX_FILE_SIZE) > (MAX_FILE_SIZE - fileLength)))
{
fileNumber++;
fileLength = 0;
}
fileLength += chapter.allVerses.length();
chapter.fileNumber = fileNumber;
output.write(fileNumber);
output.writeInt(chapter.allVerses.length());
}
output.write(chapter.verses.size());
}
}
output.close();
// Get the bytes of the index so that they can be written to the JAR file
byte[] byteArray = byteArrayOutputStream.toByteArray();
jarOutputStream.putNextEntry(new JarEntry("Bible Data/Index"));
jarOutputStream.write(byteArray, 0, byteArray.length);
}
/**
* Writes the necessary verse mappings, book mappings etc. in the collection.
* @param directory Directory to place index.
* @param collection Collection to create.
**/
public static void writeMultipleMappings(JarOutputStream jarOutputStream, Collection collection, HashMap books) throws IOException
{
System.err.println("Writing out the mappings");
jarOutputStream.putNextEntry(new JarEntry("Bible Data/Reference Lookup"));
DataOutputStream jarDataOutput = new DataOutputStream(jarOutputStream);
// Verse mappings
{
int bookIndex = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(byteArrayOutputStream);
for (Book key: collection.books)
{
Book cBook = books.get(key.name);
Integer verseMappings[] = cBook.getVerseMappings();
for (Integer mapping : verseMappings) {
int mappingValue = mapping.intValue() | ((bookIndex & 0xFF) << 24);
output.writeInt(mappingValue);
System.err.println("mapping: " +
((mappingValue >> 24) & 0xFF) + " " +
((mappingValue >> 16) & 0xFF) + " " +
((mappingValue >> 8) & 0xFF) + " " +
((mappingValue) & 0xFF));
}
bookIndex++;
}
output.close();
// Get the bytes of the index so that they can be written to the JAR file
byte[] byteArray = byteArrayOutputStream.toByteArray();
jarDataOutput.write("ve".getBytes("UTF-8"));
jarDataOutput.writeInt(0);
jarDataOutput.writeInt(byteArray.length + 4);
jarDataOutput.write(byteArray);
jarDataOutput.write(new byte[] {-1, -1, -1, -1});
}
// Book mappings (TODO)
{
int bookIndex = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(byteArrayOutputStream);
for (Book key: collection.books)
{
Book cBook = books.get(key.name);
if (cBook.bookNumber != -1) {
output.writeShort(
((bookIndex & 0xFF) << 8) |
((cBook.bookNumber & 0xFF)));
}
bookIndex ++;
}
output.close();
byte[] byteArray = byteArrayOutputStream.toByteArray();
jarDataOutput.write("bk".getBytes("UTF-8"));
jarDataOutput.writeInt(0);
jarDataOutput.writeInt(byteArray.length + 2);
jarDataOutput.write(byteArray);
jarDataOutput.write(new byte[] {-1, -1});
}
jarDataOutput.write(new byte[] {0,0});
}
/**
* Writes out the actual book data.
* @param directory Directory to place the Go Bible data.
* @param collection Collection to create.
* @param books Books from the XML file.
**/
public static void writeMultipleBooks(JarOutputStream jarOutputStream, Collection collection, HashMap books) throws IOException
{
// Write the number of chapters and verses for each book
for (Enumeration e = collection.books.elements(); e.hasMoreElements(); )
{
Book collectionBook = (Book) e.nextElement();
// Get the book from the XML file
Book thmlBook = (Book) books.get(collectionBook.name);
System.out.print(thmlBook.fileName + ", ");
// Write out the book index
writeMultipleBookIndex(jarOutputStream, collectionBook, thmlBook);
DataOutputStream dataOutputStream = new DataOutputStream(jarOutputStream);
int fileNumber = 0;
StringBuffer buffer = new StringBuffer();
for (int chapterNumber = collectionBook.startChapter; chapterNumber <= collectionBook.endChapter; chapterNumber++)
{
Chapter chapter = (Chapter) thmlBook.chapters.elementAt(chapterNumber - thmlBook.startChapter);
// Write out the chapter
if (COMBINED_CHAPTERS)
{
// If the file number has changed then write out the file
if (chapter.fileNumber != fileNumber)
{
//byte[] byteArray = buffer.toString().getBytes("UTF-8");
//System.out.println("Writing " + thmlBook.name + " " + fileNumber + " - " + byteArray.length + " bytes");
jarOutputStream.putNextEntry(new JarEntry("Bible Data/" + thmlBook.fileName + "/" + thmlBook.fileName + " " + fileNumber));
//jarOutputStream.write(byteArray, 0, byteArray.length);
//dataOutputStream.writeUTF(buffer.toString());
// Convert the StringBuffer to bytes
byte[] verseBytes = buffer.toString().getBytes("UTF-8");
dataOutputStream.writeInt(verseBytes.length);
dataOutputStream.write(verseBytes, 0, verseBytes.length);
fileNumber = chapter.fileNumber;
buffer = new StringBuffer();
}
buffer.append(chapter.allVerses.toString());
}
else
{
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(byteArrayOutputStream);
String allVerses = chapter.allVerses.toString();
//System.out.println("Writing " + thmlBook.name + " " + chapterNumber + " - " + allVerses.length() + " bytes");
output.writeUTF(allVerses);
output.close();
byte[] byteArray = byteArrayOutputStream.toByteArray();
jarOutputStream.putNextEntry(new JarEntry("Bible Data/" + thmlBook.fileName + "/" + thmlBook.fileName + " " + chapterNumber));
jarOutputStream.write(byteArray, 0, byteArray.length);
}
}
if (COMBINED_CHAPTERS)
{
// Write out the final file
//byte[] byteArray = buffer.toString().getBytes("UTF-8");
//System.out.println("Writing " + thmlBook.name + " " + fileNumber + " - " + byteArray.length + " bytes");
jarOutputStream.putNextEntry(new JarEntry("Bible Data/" + thmlBook.fileName + "/" + thmlBook.fileName + " " + fileNumber));
//jarOutputStream.write(byteArray, 0, byteArray.length);
//dataOutputStream.writeUTF(buffer.toString());
// Convert the StringBuffer to bytes
byte[] verseBytes = buffer.toString().getBytes("UTF-8");
dataOutputStream.writeInt(verseBytes.length);
dataOutputStream.write(verseBytes, 0, verseBytes.length);
}
}
}
/**
* Writes out the index for a book.
* @param collectionBook Contains the book start and end chapters to write out.
* @param xmlBook Contains the book data to write out.
**/
public static void writeMultipleBookIndex(/*File directory,*/ JarOutputStream jarOutputStream, Book collectionBook, Book xmlBook) throws IOException
{
// Create index file
//DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(new File(directory, "Index"))));
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(byteArrayOutputStream);
// Write the verse sizes for every chapter
for (int chapterNumber = collectionBook.startChapter; chapterNumber <= collectionBook.endChapter; chapterNumber++)
{
Chapter chapter = (Chapter) xmlBook.chapters.elementAt(chapterNumber - xmlBook.startChapter);
for (Enumeration e = chapter.verses.elements(); e.hasMoreElements(); )
{
String verse = (String) e.nextElement();
output.writeShort(verse.length());
}
}
output.close();
byte[] byteArray = byteArrayOutputStream.toByteArray();
jarOutputStream.putNextEntry(new JarEntry("Bible Data/" + xmlBook.fileName + "/Index"));
jarOutputStream.write(byteArray, 0, byteArray.length);
}
/**
* Goes through all of the characters in the verses and generates font bitmaps for them.
* This is currently experimental and shouldn't be used for any production collections.
**/
public static void generateCustomFont(String customFontString, File collectionsFile, HashMap books) throws IOException
{
System.out.println("Generating fonts...");
// Create a glyph directory
File glyphDirectory = new File(collectionsFile.getParent(), "glyphs");
if (!glyphDirectory.exists())
{
glyphDirectory.mkdir();
}
Font font = new Font(customFontString, Font.PLAIN, 14);
BufferedImage testFontImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D testGraphics = (Graphics2D) testFontImage.getGraphics();
testGraphics.setFont(font);
FontMetrics fontMetrics = testGraphics.getFontMetrics();
int ascent = fontMetrics.getAscent();
FontRenderContext testFontRenderContext = testGraphics.getFontRenderContext();
HashMap glyphs = new HashMap();
for (Object bookObject: books.values())
{
Book book = (Book) bookObject;
for (Object chapterObject: book.chapters)
{
Chapter chapter = (Chapter) chapterObject;
char[] charArray = chapter.allVerses.toString().toCharArray();
int startIndex = 0;
int c = 0;
// Go through verse data and convert characters not yet converted
for (int i = 0; i < charArray.length; i++)
{
c |= charArray[i];
// If next character will be a Tamil vowel then skip to next character
if (i < charArray.length - 1 && charArray[i + 1] >= '\u0BBE' && charArray[i + 1] <= '\u0BCD')
{
startIndex = i;
c <<= 16;
continue;
}
if (!glyphs.containsKey(c))
{
Rectangle2D stringBounds = font.getStringBounds(charArray, startIndex, i + 1, testFontRenderContext);
String hexString = Integer.toHexString(c);
System.out.println(hexString + ": " + ((char) c) + ", height: " + fontMetrics.getHeight());
BufferedImage fontImage = new BufferedImage((int) stringBounds.getWidth(), fontMetrics.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = (Graphics2D) fontImage.getGraphics();
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics.setColor(Color.BLACK);
graphics.setFont(font);
// Convert character to a bitmap
graphics.drawString(new String(charArray, startIndex, i - startIndex + 1), 0, ascent);
//ImageIO.write(fontImage, "PNG", new File(glyphDirectory, hexString + ".png"));
glyphs.put(c, fontImage);
}
startIndex = i + 1;
c = 0;
}
}
}
// Go through HashMap and generate new HashMap of glyph indexes.
HashMap glyphIndexes = new HashMap();
int index = 0;
for (int key: glyphs.keySet())
{
glyphIndexes.put(key, index);
ImageIO.write(glyphs.get(key), "PNG", new File(glyphDirectory, index + ".png"));
index++;
}
System.out.println("Fonts generated.");
}
}