// Written in the D programming language /* ///////////////////////////////////////////////////////////////////////////// * File: std/openrj.d * * Purpose: Open-RJ/D mapping for the D standard library * * Created: 11th June 2004 * Updated: 10th March 2005 * * Home: http://openrj.org/ * * Copyright 2004-2005 by Matthew Wilson and Synesis Software * Written by Matthew Wilson * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, in both source and binary form, subject to the following * restrictions: * * - The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * - Altered source versions must be plainly marked as such, and must not * be misrepresented as being the original software. * - This notice may not be removed or altered from any source * distribution. * * ////////////////////////////////////////////////////////////////////////// * Altered by Walter Bright. */ /** * Open-RJ mapping for the D standard library. * * Authors: * Matthew Wilson * References: * $(LINK2 http://www.$(OPENRJ).org/, Open-RJ) * Macros: * WIKI=Phobos/StdOpenrj * OPENRJ=openrj */ /* ///////////////////////////////////////////////////////////////////////////// * Module */ module std.openrj; /* ///////////////////////////////////////////////////////////////////////////// * Imports */ private import std.ctype; version(MainTest) { private import std.file; private import std.perf; } // version(MainTest) private import std.string; /* ///////////////////////////////////////////////////////////////////////////// * Version information */ // This'll be moved out to somewhere common soon private struct Version { string name; string description; uint major; uint minor; uint revision; uint edit; ulong buildTime; } public static Version VERSION = { "std.openrj" , "Record-JAR database reader" , 1 , 0 , 7 , 7 , 0 }; /* ///////////////////////////////////////////////////////////////////////////// * Structs */ // This'll be moved out to somewhere common soon private struct EnumString { int value; string str; }; private template enum_to_string(T) { string enum_to_string(const EnumString[] strings, T t) { // 'Optimised' search. // // Since many enums start at 0 and are contiguously ordered, it's quite // likely that the value will equal the index. If it does, we can just // return the string from that index. int index = cast(int)(t); if( index >= 0 && index < strings.length && strings[index].value == index) { return strings[index].str.idup; } // Otherwise, just do a linear search foreach(s; strings) { if(cast(int)(t) == s.value) { return s.str.idup; } } return ""; } } /* ///////////////////////////////////////////////////////////////////////////// * Enumerations */ /** Flags that moderate the creation of Databases */ public enum ORJ_FLAG { ORDER_FIELDS = 0x0001, /// Arranges the fields in alphabetical order ELIDE_BLANK_RECORDS = 0x0002, /// Causes blank records to be ignored } /** * */ public string toString(ORJ_FLAG f) { static const EnumString strings[] = [ { ORJ_FLAG.ORDER_FIELDS, "Arranges the fields in alphabetical order" } , { ORJ_FLAG.ELIDE_BLANK_RECORDS, "Causes blank records to be ignored" } ]; return enum_to_string!(ORJ_FLAG)(strings, f); } /** General error codes */ public enum ORJRC { SUCCESS = 0, /// Operation was successful CANNOT_OPEN_JAR_FILE, /// The given file does not exist, or cannot be accessed NO_RECORDS, /// The database file contained no records OUT_OF_MEMORY, /// The API suffered memory exhaustion BAD_FILE_READ, /// A read operation failed PARSE_ERROR, /// Parsing of the database file failed due to a syntax error INVALID_INDEX, /// An invalid index was specified UNEXPECTED, /// An unexpected condition was encountered INVALID_CONTENT, /// The database file contained invalid content } /** * */ public string toString(ORJRC f) { static const EnumString strings[] = [ { ORJRC.SUCCESS, "Operation was successful" } , { ORJRC.CANNOT_OPEN_JAR_FILE, "The given file does not exist, or cannot be accessed" } , { ORJRC.NO_RECORDS, "The database file contained no records" } , { ORJRC.OUT_OF_MEMORY, "The API suffered memory exhaustion" } , { ORJRC.BAD_FILE_READ, "A read operation failed" } , { ORJRC.PARSE_ERROR, "Parsing of the database file failed due to a syntax error" } , { ORJRC.INVALID_INDEX, "An invalid index was specified" } , { ORJRC.UNEXPECTED, "An unexpected condition was encountered" } , { ORJRC.INVALID_CONTENT, "The database file contained invalid content" } ]; return enum_to_string!(ORJRC)(strings, f); } /** Parsing error codes */ public enum ORJ_PARSE_ERROR { SUCCESS = 0, /// Parsing was successful RECORD_SEPARATOR_IN_CONTINUATION, /// A record separator was encountered during a content line continuation UNFINISHED_LINE, /// The last line in the database was not terminated by a line-feed UNFINISHED_FIELD, /// The last field in the database file was not terminated by a record separator UNFINISHED_RECORD, /// The last record in the database file was not terminated by a record separator } /** * */ public string toString(ORJ_PARSE_ERROR f) { static const EnumString strings[] = [ { ORJ_PARSE_ERROR.SUCCESS, "Parsing was successful" } , { ORJ_PARSE_ERROR.RECORD_SEPARATOR_IN_CONTINUATION, "A record separator was encountered during a content line continuation" } , { ORJ_PARSE_ERROR.UNFINISHED_LINE, "The last line in the database was not terminated by a line-feed" } , { ORJ_PARSE_ERROR.UNFINISHED_FIELD, "The last field in the database file was not terminated by a record separator" } , { ORJ_PARSE_ERROR.UNFINISHED_RECORD, "The last record in the database file was not terminated by a record separator" } ]; return enum_to_string!(ORJ_PARSE_ERROR)(strings, f); } /* ///////////////////////////////////////////////////////////////////////////// * Classes */ /** * */ class OpenRJException : public Exception { /* \name Construction */ protected: this(string message) { super(message); } } /** * */ class DatabaseException : public OpenRJException { /* \name Construction */ private: this(string details, ORJRC rc) { //printf("DatabaseException(0: %.*s, %.*s)\n", details, std.openrj.toString(rc)); string message = std.string.format( "Database creation failed; error: %s, %s" , cast(int)rc , std.openrj.toString(rc)); m_rc = rc; m_pe = ORJ_PARSE_ERROR.SUCCESS; m_lineNum = -1; super(message); } this(ORJRC rc, int lineNum) { //printf("DatabaseException(1: %.*s, %d)\n", std.openrj.toString(rc), lineNum); string message = std.string.format( "Database creation failed, at line %s; error: %s, %s" , lineNum , cast(int)rc , std.openrj.toString(rc)); m_rc = rc; m_pe = ORJ_PARSE_ERROR.SUCCESS; m_lineNum = lineNum; super(message); } this(ORJ_PARSE_ERROR pe, int lineNum) { //printf("DatabaseException(2: %.*s, %d)\n", std.openrj.toString(pe), lineNum); string message = std.string.format( "Parsing error in database, at line %s; parse error: %s, %s" , lineNum , cast(int)pe , std.openrj.toString(pe)); m_rc = ORJRC.PARSE_ERROR; m_pe = pe; m_lineNum = lineNum; super(message); } this(string details, ORJ_PARSE_ERROR pe, int lineNum) { //printf("DatabaseException(3: %.*s, %.*s, %d)\n", details, std.openrj.toString(rc), lineNum); string message = std.string.format( "Parsing error in database, at line %s; parse error: %s, %s; %s" , lineNum , cast(int)pe , std.openrj.toString(pe) , details); m_rc = ORJRC.PARSE_ERROR; m_pe = pe; m_lineNum = lineNum; super(message); } /* \name Attributes */ public: /** * */ ORJRC rc() { return m_rc; } /** * */ ORJ_PARSE_ERROR parseError() { return m_pe; } /** * */ int lineNum() { return m_lineNum; } // Members private: int m_lineNum; ORJRC m_rc; ORJ_PARSE_ERROR m_pe; } /** * */ class InvalidKeyException : public OpenRJException { /* \name Construction */ private: this(string message) { super(message); } } /** * */ class InvalidTypeException : public OpenRJException { /* \name Construction */ private: this(string message) { super(message); } } /* ///////////////////////////////////////////////////////////////////////////// * Classes */ /// Represents a field in the database class Field { /* \name Construction */ private: this(string name, string value/* , Record record */) in { assert(null !is name); assert(null !is value); } body { m_name = name; m_value = value; /* m_record = record; */ } /* \name Attributes */ public: /** * */ final string name() { return m_name; } /** * */ final string value() { return m_value; } /** * */ Record record() { return m_record; } /* \name Comparison */ /+ public: int opCmp(Object rhs) { Field f = cast(Field)(rhs); if(null is f) { throw new InvalidTypeException("Attempt to compare a Field with an instance of another type"); } return opCmp(f); } public: int opCmp(Field rhs) { int res; if(this is rhs) { res = 0; } else { res = std.string.cmp(m_name, rhs.m_name); if(0 == res) { res = std.string.cmp(m_value, rhs.m_value); } } return res; } +/ // Members private: string m_name; string m_value; Record m_record; } /// Represents a record in the database, consisting of a set of fields class Record { /* \name Types */ public: alias object.size_t size_type; alias object.size_t index_type; alias object.ptrdiff_t difference_type; /* \name Construction */ private: this(Field[] fields, uint flags, Database database) { m_fields = fields.dup; if(flags & ORJ_FLAG.ORDER_FIELDS) { m_fields = m_fields.sort; } foreach(Field field; m_fields) { if(!(field.name in m_values)) { m_values[field.name] = field; } } m_database = database; } /* \name Attributes */ public: /** * */ uint numFields() { return m_fields.length; } /** * */ uint length() { return numFields(); } /** * */ Field[] fields() { return m_fields.dup; } /** * */ Field opIndex(index_type index) in { assert(index < m_fields.length); } body { return m_fields[index]; } /** * */ string opIndex(string fieldName) { return getField(fieldName).value; } /** * */ Field getField(string fieldName) in { assert(null !is fieldName); } body { Field field = findField(fieldName); if(null is field) { throw new InvalidKeyException("field not found"); } return field; } /** * */ Field findField(string fieldName) in { assert(null !is fieldName); } body { Field *pfield = (fieldName in m_values); return (null is pfield) ? null : *pfield; } /** * */ int hasField(string fieldName) { return null !is findField(fieldName); } /** * */ Database database() { return m_database; } /* \name Enumeration */ public: /** * */ int opApply(int delegate(inout Field field) dg) { int result = 0; foreach (inout field; m_fields) { result = dg(field); if(0 != result) { break; } } return result; } /** * */ int opApply(int delegate(in string name, in string value) dg) { int result = 0; foreach(Field field; m_fields) { result = dg(field.name(), field.value()); if(0 != result) { break; } } return result; } // Members private: Field[] m_fields; Field[string] m_values; Database m_database; } /** * */ class Database { /* \name Types */ public: alias object.size_t size_type; alias object.size_t index_type; alias object.ptrdiff_t difference_type; /* \name Construction */ private: void init_(string[] lines, uint flags) { // Enumerate int bContinuing = false; Field[] fields; string nextLine; int lineNum = 1; int nextLineNum = 1; foreach(string ln; lines) { // Always strip trailing space auto line = stripr(ln); // Check that we don't start a continued line with a record separator if( bContinuing && line.length > 1 && "%%" == line[0 .. 2]) { throw new DatabaseException(ORJ_PARSE_ERROR.RECORD_SEPARATOR_IN_CONTINUATION, lineNum); } // Always strip leading whitespace line = stripl(line); int bContinuationLine; if( line.length > 0 && '\\' == line[line.length - 1]) { bContinuationLine = true; bContinuing = true; line = line[0 .. line.length - 1]; } // Always add on to the previous line nextLine = nextLine ~ line; line = null; if(!bContinuationLine) { if(0 == nextLine.length) { // Just ignore these lines } else if(1 == nextLine.length) { throw new DatabaseException(ORJ_PARSE_ERROR.UNFINISHED_FIELD, lineNum); } else { if("%%" == nextLine[0 .. 2]) { // Comment line - terminate the record // printf("-- record --\n"); if( 0 != fields.length || 0 == (ORJ_FLAG.ELIDE_BLANK_RECORDS & flags)) { Record record = new Record(fields, flags, this); foreach(Field field; fields) { field.m_record = record; } m_records ~= record; fields = null; } } else { int colon = find(nextLine, ':'); if(-1 == colon) { throw new DatabaseException(ORJ_PARSE_ERROR.UNFINISHED_FIELD, lineNum); } // printf("%.*s(%d): %.*s (%d)\n", file, nextLineNum, nextLine, colon); string name = nextLine[0 .. colon]; string value = nextLine[colon + 1 .. nextLine.length]; name = stripr(name); value = stripl(value); // printf("%.*s(%d): %.*s=%.*s\n", file, nextLineNum, name, value); Field field = new Field(name, value); fields ~= field; m_fields ~= field; } } nextLine = ""; nextLineNum = lineNum + 1; bContinuing = false; } /+ // This is currently commented out as it seems unlikely to be sensible to // order the Fields globally. The reasoning is that if the Fields are used // globally then it's more likely that their ordering in the database source // is meaningful. If someone really needs an all-Fields array ordered, they // can do it manually. if(flags & ORJ_FLAG.ORDER_FIELDS) { m_fields = m_fields.sort; } +/ ++lineNum; } if(bContinuing) { throw new DatabaseException(ORJ_PARSE_ERROR.UNFINISHED_LINE, lineNum); } if(fields.length > 0) { throw new DatabaseException(ORJ_PARSE_ERROR.UNFINISHED_RECORD, lineNum); } if(0 == m_records.length) { throw new DatabaseException(ORJRC.NO_RECORDS, lineNum); } m_flags = flags; m_numLines = lines.length; } public: /** * */ this(string memory, uint flags) { string[] lines = split(memory, "\n"); init_(lines, flags); } /** * */ this(string[] lines, uint flags) { init_(lines, flags); } /* \name Attributes */ public: /** * */ size_type numRecords() { return m_records.length; } /** * */ size_type numFields() { return m_fields.length; } /** * */ size_type numLines() { return m_numLines; } /* \name Attributes */ public: /** * */ uint flags() { return m_flags; } /** * */ Record[] records() { return m_records.dup; } /** * */ Field[] fields() { return m_fields.dup; } /** * */ uint length() { return numRecords(); } /** * */ Record opIndex(index_type index) in { assert(index < m_records.length); } body { return m_records[index]; } /* \name Searching */ public: /** * */ Record[] getRecordsContainingField(string fieldName) { Record[] records; foreach(Record record; m_records) { if(null !is record.findField(fieldName)) { records ~= record; } } return records; } /** * */ Record[] getRecordsContainingField(string fieldName, string fieldValue) { Record[] records; uint flags = flags; foreach(Record record; m_records) { Field field = record.findField(fieldName); if(null !is field) { // Since there can be more than one field with the same name in // the same record, we need to search all fields in this record if(ORJ_FLAG.ORDER_FIELDS == (flags & ORJ_FLAG.ORDER_FIELDS)) { // We can do a sorted search foreach(Field field; record) { int res = cmp(field.name, fieldName); if( 0 == res && ( null is fieldValue || field.value == fieldValue)) { records ~= record; break; } else if(res > 0) { break; } } } else { foreach(Field field; record) { if( field.name == fieldName && ( null is fieldValue || field.value == fieldValue)) { records ~= record; break; } } } } } return records; } /* \name Enumeration */ public: /** * */ int opApply(int delegate(inout Record record) dg) { int result = 0; foreach(inout Record record; m_records) { result = dg(record); if(0 != result) { break; } }; return result; } /** * */ int opApply(int delegate(inout Field field) dg) { int result = 0; foreach(inout Field field; m_fields) { result = dg(field); if(0 != result) { break; } }; return result; } // Members private: uint m_flags; size_type m_numLines; Record[] m_records; Field[] m_fields; } /* ////////////////////////////////////////////////////////////////////////// */ version(MainTest) { int main(string[] args) { int flags = 0 | ORJ_FLAG.ORDER_FIELDS | ORJ_FLAG.ELIDE_BLANK_RECORDS | 0; if(args.length < 2) { printf("Need to specify jar file\n"); } else { PerformanceCounter counter = new PerformanceCounter(); try { printf( "std.openrj test:\n\tmodule: \t%.*s\n\tdescription: \t%.*s\n\tversion: \t%d.%d.%d.%d\n" , std.openrj.VERSION.name , std.openrj.VERSION.description , std.openrj.VERSION.major , std.openrj.VERSION.minor , std.openrj.VERSION.revision , std.openrj.VERSION.edit); counter.start(); string file = args[1]; string chars = cast(string)std.file.read(file); Database database = new Database(chars, flags); // Database database = new Database(split(chars, "\n"), flags); counter.stop(); PerformanceCounter.interval_type loadTime = counter.microseconds(); counter.start(); int i = 0; foreach(Record record; database.records) { foreach(Field field; record) { i += field.name.length + field.value.length; } } counter.stop(); PerformanceCounter.interval_type enumerateTime = counter.microseconds(); printf("Open-RJ/D test: 100%%-D!!\n"); printf("Load time: %ld\n", loadTime); printf("Enumerate time: %ld\n", enumerateTime); return 0; printf("Records (%u)\n", database.numRecords); foreach(Record record; database) { printf(" Record\n"); foreach(Field field; record.fields) { printf(" Field: %.*s=%.*s\n", field.name, field.value); } } printf("Fields (%u)\n", database.numFields); foreach(Field field; database) { printf(" Field: %.*s=%.*s\n", field.name, field.value); } Record[] records = database.getRecordsContainingField("Name"); printf("Records containing 'Name' (%u)\n", records); foreach(Record record; records) { printf(" Record\n"); foreach(Field field; record.fields) { printf(" Field: %.*s=%.*s\n", field.name, field.value); } } } catch(Exception x) { printf("Exception: %.*s\n", x.toString()); } } return 0; } } // version(MainTest) /* ////////////////////////////////////////////////////////////////////////// */