AppleCommander/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/filters/AppleWorksDataBaseFileFilte...

288 lines
10 KiB
Java

/*
* AppleCommander - An Apple ][ image utility.
* Copyright (C) 2002-2022 by Robert Greene
* robgreene at users.sourceforge.net
*
* This program 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 of the License, or (at your
* option) any later version.
*
* This program 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 along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.webcodepro.applecommander.storage.filters;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import com.webcodepro.applecommander.storage.FileEntry;
import com.webcodepro.applecommander.storage.FileFilter;
import com.webcodepro.applecommander.storage.StorageBundle;
import com.webcodepro.applecommander.util.AppleUtil;
import com.webcodepro.applecommander.util.TextBundle;
/**
* Export an AppleWorks database file to a text file.
* Current export is limited to a standard comma-separated file (CSV)
* file format which should be suitable to load into a database or a
* spreadsheet.
* <p>
* Data base files start with a variable length header, followed by
* 600 bytes for each report format (if any), the standard values
* record, then variable length information for each record. Note that
* the first data record contains the standard (presumably default)
* values. The category entries are in the same order that the category
* names appear in the header record.
* <p>
* See: http://www.gno.org/pub/apple2/doc/apple/filetypes/ftn.19.xxxx
* <p>
* Date Created: February 15, 2003
* @author Rob Greene
*/
public class AppleWorksDataBaseFileFilter implements FileFilter {
private TextBundle textBundle = StorageBundle.getInstance();
/**
* The number of bytes in the remainder of the header record.
*/
private static final int HEADER_LENGTH_WORD = 0;
/**
* Number of categories per record. Values from $01 to $1E.
*/
private static final int HEADER_CATEGORIES_BYTE = 35;
/**
* Number of records in file. If DBMinVers is non-zero,
* the high bit of this word may be set. If it is,
* there are more than eight reports and the remaining
* 15 bits contain the true number of records defined.
*/
private static final int HEADER_RECORDS_WORD = 36;
/**
* Number of reports in a file, maximum of 8 (20 for AW 3.0).
*/
private static final int HEADER_REPORTS_BYTE = 38;
/**
* DBMinVers. The minimum version of AppleWorks needed
* to read this file. This will be $0 unless there are
* more than 8 report formats; it will then contain the
* version number 30 ($1E) or greater.
*/
private static final int HEADER_DBMINVERS_BYTE = 218;
/**
* Name of category. Maximum length of 20 bytes. If this
* is the last category in the file, the header record will
* end here. This record is 22 bytes in length and will
* be repeated up to $1E times.
*/
private static final int HEADER_CATEGORY_STRING = 357;
/**
* Each category name takes up a maximum of 22 bytes.
*/
private static final int HEADER_CATEGORY_LENGTH = 22;
/**
* Each report is defined in 600 bytes. The current file
* filter does not deal with reports.
*/
private static final int REPORT_LENGTH = 600;
/**
* Count of the number of bytes in the remainder of the
* data record.
*/
// private static final int DATA_LENGTH_WORD = 0;
/**
* Data control record indicating that a number of
* categories need to be skipped. This (minus $80)
* is a count of the number of categories to be skipped.
* For example, $82 means skip two categories.
*/
private static final int DATA_CONTROL_SKIP = 0x80;
/**
* Indicates the end of a record.
*/
private static final int DATA_CONTROL_END = 0xff;
/**
* Indicates a date entry.
*/
private static final int SPECIAL_CONTROL_DATE = 0xc0;
/**
* ASCII year code, like "84" ($38 $34).
*/
private static final int DATE_YEAR_OFFSET = 1;
/**
* ASCII month code. "A" means January, "L" means
* December.
*/
private static final int DATE_MONTH_OFFSET = 3;
/**
* List of months used for date conversion.
*/
private static final String[] months = {
StorageBundle.getInstance().get("AppleWorksDataBaseFileFilter.January"), //$NON-NLS-1$
StorageBundle.getInstance().get("AppleWorksDataBaseFileFilter.February"), //$NON-NLS-1$
StorageBundle.getInstance().get("AppleWorksDataBaseFileFilter.March"), //$NON-NLS-1$
StorageBundle.getInstance().get("AppleWorksDataBaseFileFilter.April"), //$NON-NLS-1$
StorageBundle.getInstance().get("AppleWorksDataBaseFileFilter.May"), //$NON-NLS-1$
StorageBundle.getInstance().get("AppleWorksDataBaseFileFilter.June"), //$NON-NLS-1$
StorageBundle.getInstance().get("AppleWorksDataBaseFileFilter.July"), //$NON-NLS-1$
StorageBundle.getInstance().get("AppleWorksDataBaseFileFilter.August"), //$NON-NLS-1$
StorageBundle.getInstance().get("AppleWorksDataBaseFileFilter.September"), //$NON-NLS-1$
StorageBundle.getInstance().get("AppleWorksDataBaseFileFilter.October"), //$NON-NLS-1$
StorageBundle.getInstance().get("AppleWorksDataBaseFileFilter.November"), //$NON-NLS-1$
StorageBundle.getInstance().get("AppleWorksDataBaseFileFilter.December") //$NON-NLS-1$
};
/**
* ASCII day of the month, like "31" ($33 $31).
*/
private static final int DATE_DAY_OFFSET = 4;
/**
* The length of a date is 6 bytes.
*/
private static final int DATE_LENGTH = 6;
/**
* Indicates a time entry.
*/
private static final int SPECIAL_CONTROL_TIME = 0xd4;
/**
* ASCII hour code. "A" means 00 (the hour after
* midnight). "X" means 23, the hour before midnight.
*/
private static final int TIME_HOUR_OFFSET = 1;
/**
* ASCII minute code. Values from "00" to "59".
*/
private static final int TIME_MINUTE_OFFSET = 2;
/**
* Length of a time entry is 4 bytes.
*/
private static final int TIME_LENGTH = 4;
/**
* Create an AppleWorksDataBaseFileFilter.
*/
public AppleWorksDataBaseFileFilter() {
super();
}
/**
* Process the given FileEntry and return a byte array with filtered data.
* @see com.webcodepro.applecommander.storage.FileFilter#filter(FileEntry)
*/
public byte[] filter(FileEntry fileEntry) {
byte[] fileData = fileEntry.getFileData();
ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
PrintWriter printWriter = new PrintWriter(byteArray, true);
// process header information:
int headerLength = AppleUtil.getWordValue(fileData, HEADER_LENGTH_WORD)
+ 2; // does not include this word!
int categoryCount = AppleUtil.getUnsignedByte(
fileData[HEADER_CATEGORIES_BYTE]);
int recordCount = AppleUtil.getWordValue(fileData, HEADER_RECORDS_WORD);
int reportCount = AppleUtil.getUnsignedByte(
fileData[HEADER_REPORTS_BYTE]);
int dbMinVers = AppleUtil.getUnsignedByte(
fileData[HEADER_DBMINVERS_BYTE]);
if (dbMinVers > 0x00 && (recordCount & 0x8000) != 0) {
recordCount &= 0x7fff; // adjust for "more than 8 reports" flag
}
int offset = HEADER_CATEGORY_STRING;
for (int i=0; i<categoryCount; i++) {
String name = AppleUtil.getProdosString(fileData, offset);
if (i > 0) printWriter.print(","); //$NON-NLS-1$
printWriter.print('"');
printWriter.print(name);
printWriter.print('"');
offset+= HEADER_CATEGORY_LENGTH;
}
printWriter.println();
if (offset != headerLength) {
throw new IndexOutOfBoundsException(
textBundle.get("AppleWorksDataBaseFileFilter.InvalidHeaderLengthError")); //$NON-NLS-1$
}
// skip reports:
offset+= (reportCount * REPORT_LENGTH);
// process data:
for (int i=0; i<recordCount+1; i++) {
// length = record length + 2 bytes for the length word itself.
int length = AppleUtil.getWordValue(fileData, offset) + 2;
int data = offset + 2;
int column = 0;
while (AppleUtil.getUnsignedByte(fileData[data]) != DATA_CONTROL_END) {
if (column > 0) printWriter.print(',');
int controlByte = AppleUtil.getUnsignedByte(fileData[data]);
if (controlByte < DATA_CONTROL_SKIP) {
String string = AppleUtil.getPascalString(fileData, data);
int specialChar = AppleUtil.getUnsignedByte(fileData[data+1]);
data+= string.length() + 1;
switch (specialChar) {
case SPECIAL_CONTROL_DATE:
convertDate(printWriter, string);
break;
case SPECIAL_CONTROL_TIME:
convertTime(printWriter, string);
break;
default:
printWriter.print('"');
printWriter.print(string);
printWriter.print('"');
break;
}
} else {
int repeats = controlByte - DATA_CONTROL_SKIP;
while (repeats > 0) {
printWriter.print("\",\""); //$NON-NLS-1$
repeats--;
}
data++; // move on to the next column
}
column++;
}
offset+= length;
printWriter.println();
}
// return CSV file:
return byteArray.toByteArray();
}
/**
* Give suggested file name.
* @see com.webcodepro.applecommander.storage.FileFilter#getSuggestedFileName(FileEntry)
*/
public String getSuggestedFileName(FileEntry fileEntry) {
return fileEntry.getFilename() + ".csv"; //$NON-NLS-1$
}
/**
* Convert the date entry.
*/
protected void convertDate(PrintWriter printWriter, String date) {
if (date.length() != DATE_LENGTH) {
printWriter.print(textBundle.
format("AppleWorksDataBaseFileFilter.InvalidDate", date)); //$NON-NLS-1$
}
printWriter.print((char)('0' + (date.charAt(DATE_YEAR_OFFSET) - 0x30)));
printWriter.print((char)('0' + (date.charAt(DATE_YEAR_OFFSET + 1) - 0x30)));
printWriter.print('-');
printWriter.print(months[date.charAt(DATE_MONTH_OFFSET) - 'A'].substring(0,3));
printWriter.print('-');
printWriter.print((char)('0' + (date.charAt(DATE_DAY_OFFSET) - 0x30)));
printWriter.print((char)('0' + (date.charAt(DATE_DAY_OFFSET + 1) - 0x30)));
}
/**
* Convert the time entry.
*/
protected void convertTime(PrintWriter printWriter, String time) {
if (time.length() != TIME_LENGTH) {
printWriter.print(textBundle.
format("AppleWorksDataBaseFileFilter.InvalidTime", time)); //$NON-NLS-1$
}
printWriter.print(time.charAt(TIME_HOUR_OFFSET) - 'A');
printWriter.print(':');
printWriter.print((char)('0' + time.charAt(TIME_MINUTE_OFFSET) - 0x30));
printWriter.print((char)('0' + time.charAt(TIME_MINUTE_OFFSET + 1) - 0x30));
}
}