package net.sf.jpluck.plucker;
import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.sf.jpluck.palm.Database;
import net.sf.jpluck.palm.PdbOutputStream;
import net.sf.jpluck.palm.Record;
import net.sf.jpluck.util.URIUtil;
public class Document extends Database implements URIResolver {
public static final int NO_COMPRESSION = 0;
public static final int DOC_COMPRESSION = 1;
public static final int ZLIB_COMPRESSION = 2;
private Color defaultLinkColor;
private Color unresolvedLinkColor;
private Date publicationDate;
private LinkableRecord homeRecord;
private String filename;
// Used only for resolving links
private Map uriIdMap = new HashMap();
// Used for tracking duplicate additions
private Set uriSet = new HashSet();
private String author;
private String outputEncoding = "PalmLatin";
private String title;
private String[] categories = new String[0];
private boolean includeImageAltText = true;
private boolean includeURIInfo;
private boolean removeUnresolvedLinks = false;
private boolean useLinkColoring = true;
private boolean useTextColors = true;
private int compression;
public Document(String name, int compressionType) {
super(1, "Data", "Plkr");
if (name != null) {
setName(name);
}
setCompression(compressionType);
// Add an index
recordList.add(new Index());
}
public Document(String name) {
this(name, ZLIB_COMPRESSION);
}
public Document() {
this(null);
}
/**
* Sets the Document's author.
* @param author The author of the pluckerDocument.
*/
public void setAuthor(String author) {
this.author = author;
}
public String getAuthor() {
return author;
}
public void setCategories(String[] categories) {
this.categories = categories;
}
public String[] getCategories() {
String[] str = new String[categories.length];
System.arraycopy(categories, 0, str, 0, str.length);
return str;
}
public void setCompression(int compression) {
if ((compression < NO_COMPRESSION) && (compression > ZLIB_COMPRESSION)) {
throw new IllegalArgumentException("Invalid compression type " + compression);
}
this.compression = compression;
}
public int getCompression() {
return compression;
}
public void setDefaultLinkColor(Color defaultLinkColor) {
this.defaultLinkColor = defaultLinkColor;
}
public Color getDefaultLinkColor() {
return defaultLinkColor;
}
public void setHome(LinkableRecord homeRecord) {
boolean ok = false;
for (Iterator iterator = recordList.iterator(); iterator.hasNext();) {
Object obj = iterator.next();
if (obj == homeRecord) {
ok = true;
}
}
if (!ok) {
throw new IllegalStateException("This record is not part of the Document.");
}
this.homeRecord = homeRecord;
}
public void setHome(String uri) {
LinkableRecord newHome = null;
/*String alternateURI = (String) alternateURIMap.get(uri);
if (alternateURI != null) {
uri = alternateURI;
}*/
for (Iterator iterator = recordList.iterator(); iterator.hasNext();) {
Record record = (Record) iterator.next();
if (record instanceof LinkableRecord) {
LinkableRecord linkableRecord = (LinkableRecord) record;
if (linkableRecord.getURI().equals(uri)) {
newHome = (LinkableRecord) record;
break;
}
}
}
if (newHome == null) {
throw new IllegalArgumentException("LinkableRecord with URI \"" + uri +
"\" not found in the Document.");
}
this.homeRecord = newHome;
}
public void setIncludeImageAltText(boolean includeImageAltText) {
this.includeImageAltText = includeImageAltText;
}
public boolean isIncludeImageAltText() {
return includeImageAltText;
}
public void setIncludeURIInfo(boolean includeURIInfo) {
this.includeURIInfo = includeURIInfo;
}
public boolean isIncludeURIInfo() {
return includeURIInfo;
}
public LinkableRecord getLinkableRecord(String uri) {
for (Iterator iterator = recordList.iterator(); iterator.hasNext();) {
Record record = (Record) iterator.next();
if (record instanceof LinkableRecord) {
LinkableRecord linkableRecord = (LinkableRecord) record;
if (linkableRecord.getURI().equals(uri) || uri.equals(linkableRecord.getAlternateURI())) {
return (LinkableRecord) record;
}
}
}
return null;
}
public void setOutputEncoding(String string) {
outputEncoding = string;
}
public String getOutputEncoding() {
return outputEncoding;
}
public void setPublicationDate(Date publicationDate) {
this.publicationDate = publicationDate;
}
public Date getPublicationDate() {
return publicationDate;
}
public void setRemoveUnresolvedLinks(boolean removeUnresolvedLinks) {
this.removeUnresolvedLinks = removeUnresolvedLinks;
}
public boolean isRemoveUnresolvedLinks() {
return removeUnresolvedLinks;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
public void setUnresolvedLinkColor(Color unresolvedLinkColor) {
this.unresolvedLinkColor = unresolvedLinkColor;
}
public Color getUnresolvedLinkColor() {
return unresolvedLinkColor;
}
public void setUseLinkColoring(boolean useLinkColoring) {
this.useLinkColoring = useLinkColoring;
}
public boolean isUseLinkColoring() {
return useLinkColoring;
}
public void setUseTextColors(boolean useTextColors) {
this.useTextColors = useTextColors;
}
public boolean isUseTextColors() {
return useTextColors;
}
public synchronized void addRecord(DataRecord record) {
if (recordList.contains(record)) {
throw new IllegalStateException("This record has already been added to the Document.");
}
if (record instanceof LinkableRecord) {
LinkableRecord linkableRecord = (LinkableRecord) record;
String uri = linkableRecord.getURI();
if (contains(uri) || contains(linkableRecord.getAlternateURI())) {
throw new IllegalStateException("A record with the URI \"" + uri +
"\" has already been added to the Document.");
}
uriSet.add(uri);
if (linkableRecord.getAlternateURI() != null) {
uriSet.add(linkableRecord.getAlternateURI());
}
}
record.setDocument(this);
recordList.add(record);
record.setRecordId(recordList.size());
if (record instanceof TextRecord) {
TextRecord textRecord = (TextRecord) record;
TextRecord[] additionalRecords = textRecord.split();
if (!textRecord.isWrittenToTempFile()) {
textRecord.writeToTempFile();
if (additionalRecords.length > 0) {
textRecord.nextSegmentFollows = true;
}
}
for (int j = 0; j < additionalRecords.length; j++) {
TextRecord additionalRecord = additionalRecords[j];
if (j < (additionalRecords.length - 1)) {
additionalRecord.nextSegmentFollows = true;
}
addRecord(additionalRecord);
}
}
}
public boolean contains(String uri) {
return (uriSet.contains(uri));
}
// From interface URIResolver
public Link resolve(String uri) {
int idx = uri.indexOf('#');
String file = uri;
String anchor = null;
if (idx > -1) {
file = uri.substring(0, idx);
anchor = uri.substring(idx + 1);
}
Integer integer = (Integer) uriIdMap.get(file);
if (integer == null) {
return null;
}
int recordId = integer.intValue();
LinkableRecord record = getLinkableRecord(file);
if ((record != null) && (anchor != null)) {
if (record instanceof TextRecord) {
TextRecord textRecord = (TextRecord) record;
TextRecord[] subRecords = textRecord.getSubRecords();
TextRecord[] textRecords = new TextRecord[subRecords.length + 1];
textRecords[0] = textRecord;
System.arraycopy(subRecords, 0, textRecords, 1, subRecords.length);
for (int i = 0; i < textRecords.length; i++) {
textRecord = textRecords[i];
int paragraph = textRecord.resolveAnchor(anchor);
if (paragraph > -1) {
return new Link(textRecord.getRecordId(), paragraph);
}
}
// If we haven't found a matching paragraph for the anchor then we link to the first paragraph
return new Link(recordId, 0);
} else {
throw new RuntimeException("Invalid anchor link to " + record.getClass() + ". " +
"Anchor links are only allowed to TextRecords");
}
}
return new Link(recordId, -1);
}
public long write(OutputStream out) throws IOException {
long size = 0;
if (homeRecord == null) {
throw new IllegalStateException("No Home set for the Document.");
}
if ((categories != null) && (categories.length > 0)) {
DataRecord record = findDataRecord(CategoryRecord.class);
if (record == null) {
addRecord(new CategoryRecord());
}
} else {
// Remove any existing CategoryRecord
removeRecord(findDataRecord(CategoryRecord.class));
}
if ((author != null) || (title != null) || (publicationDate != null) || (outputEncoding != null)) {
DataRecord record = findDataRecord(MetadataRecord.class);
if (record == null) {
addRecord(new MetadataRecord());
}
} else {
// Remove any existing MetadataRecord
removeRecord(findDataRecord(MetadataRecord.class));
}
if (bookmarkList.size() > 0) {
DataRecord record = findDataRecord(BookmarkRecord.class);
if (record == null) {
addRecord(new BookmarkRecord());
}
} else {
removeRecord(findDataRecord(BookmarkRecord.class));
}
// Add resources
/*for (int i = 0, n = recordList.size(); i < n; i++) {
Record record = (Record) recordList.get(i);
if (record instanceof TextRecord) {
TextRecord textRecord = (TextRecord) record;
for (int j = 0, m = textRecord.getParagraphCount(); j < m; j++) {
Paragraph paragraph = textRecord.getParagraph(j);
Resource[] resources = paragraph.getResources();
for (int k = 0; k < resources.length; k++) {
Resource resource = resources[k];
resource.addToDocument(this);
}
}
}
}*/
// Construct URI ID map, this map is used for resolving URIs to record IDs during the generation process.
uriIdMap.clear();
for (Iterator iterator = recordList.iterator(); iterator.hasNext();) {
Record record = (Record) iterator.next();
if (record instanceof LinkableRecord) {
LinkableRecord linkableRecord = (LinkableRecord) record;
Integer id = new Integer(record.getRecordId());
String uri = linkableRecord.getURI();
uriIdMap.put(uri, id);
if (linkableRecord.getAlternateURI() != null) {
uriIdMap.put(linkableRecord.getAlternateURI(), id);
}
}
}
if (includeURIInfo) {
/*
If we include URI info, we must first construct URI index and data records, then write the PDB and
finally remove the URI index and data records. We do this because we might want to create multiple
versions of the Document and in between writing different PDBs we might add a record or change a setting.
*/
// Create URI index and URI data records
List uriDataRecordList = new ArrayList();
List uriList = new ArrayList();
/*
Create URI data for URIs that resolve to records in the pluckerDocument, taking into account the maximum size
of 200 URIs per URI data record.
*/
int uriRecordCount = 1 + (recordList.size() / 200) + (((recordList.size() % 200) > 0) ? 1 : 0);
for (int i = 0, j = recordList.size(); i < j; i++) {
Record record = (Record) recordList.get(i);
if (record instanceof TextRecord || record instanceof ImageRecord) {
uriList.add(((LinkableRecord) record).getURI());
} else {
uriList.add("");
}
if ((uriList.size() == 200) || (i == (j - 1))) {
URIDataRecord uriDataRecord = new URIDataRecord((String[]) uriList.toArray(new String[uriList.size()]),
record.getRecordId());
uriDataRecordList.add(uriDataRecord);
uriList.clear();
}
}
// Create a list of unresolved links.
List unresolvedLinkList = new ArrayList();
Set uriSet = uriIdMap.keySet();
for (Iterator iterator = recordList.iterator(); iterator.hasNext();) {
Record record = (Record) iterator.next();
if (record instanceof TextRecord) {
TextRecord textRecord = (TextRecord) record;
String[] uris = textRecord.getLinkURIs();
for (int i = 0; i < uris.length; i++) {
String uri = URIUtil.removeAnchor(uris[i]);
if (!uriSet.contains(uri) && !unresolvedLinkList.contains(uri)) {
unresolvedLinkList.add(uri);
}
}
}
}
/*
Add the unresolved links to the URI data. We use dummy record IDs, beyond the range of actual
record IDs, for the URI data
*/
uriRecordCount += ((unresolvedLinkList.size() / 200) +
(((unresolvedLinkList.size() % 200) > 0) ? 1 : 0));
for (int i = 0; i < uriRecordCount; i++) {
uriList.add("");
}
int dummyRecordId = recordList.size() + uriRecordCount + 1;
for (int i = 0, j = unresolvedLinkList.size(); i < j; i++) {
String uri = (String) unresolvedLinkList.get(i);
uriList.add(uri);
uriIdMap.put(uri, new Integer(dummyRecordId++));
if ((uriList.size() == 200) || (i == (j - 1))) {
URIDataRecord uriDataRecord = new URIDataRecord((String[]) uriList.toArray(new String[uriList.size()]),
dummyRecordId++);
uriDataRecordList.add(uriDataRecord);
uriList.clear();
}
}
URIDataRecord[] uriDataRecords = (URIDataRecord[]) uriDataRecordList.toArray(new URIDataRecord[uriDataRecordList.size()]);
// Add URI index record and the URI data records
URIIndexRecord uriIndexRecord = new URIIndexRecord(uriDataRecords);
addRecord(uriIndexRecord);
for (int i = 0; i < uriDataRecords.length; i++) {
URIDataRecord uriDataRecord = uriDataRecords[i];
addRecord(uriDataRecord);
}
try {
// Write the PDB
size = super.write(out);
} finally {
// Now remove the URI index and data records
removeRecord(uriIndexRecord);
for (int i = 0; i < uriDataRecords.length; i++) {
URIDataRecord uriDataRecord = uriDataRecords[i];
removeRecord(uriDataRecord);
}
}
} else {
// If we do not include URI info we can just write the PDB without any special considerations.
size = super.write(out);
}
for (int i = 0; i < recordList.size(); i++) {
Record record = (Record) recordList.get(i);
if (record instanceof ImageRecord) {
ImageRecord imageRecord = (ImageRecord) record;
if (imageRecord.getURI().endsWith(".fullsize")) {
removeRecord(imageRecord);
i--;
}
}
}
return size;
}
/**
* @return A boolean indicating whether the Document is DOC-compressed or not.
*/
boolean isDocCompressed() {
return (compression == DOC_COMPRESSION);
}
boolean isZLIBCompressed() {
return (compression == ZLIB_COMPRESSION);
}
private DataRecord findDataRecord(Class clazz) {
for (Iterator iterator = recordList.iterator(); iterator.hasNext();) {
Object obj = iterator.next();
if (obj instanceof DataRecord) {
DataRecord record = (DataRecord) obj;
if (record.getClass().equals(clazz)) {
return record;
}
}
}
return null;
}
private synchronized void removeRecord(DataRecord record) {
if (record == null) {
return;
}
int idx = recordList.indexOf(record);
if (idx == -1) {
throw new IllegalStateException("This record is not part of the Document.");
}
recordList.remove(record);
for (int i = idx, j = recordList.size(); i < j; i++) {
DataRecord dataRecord = (DataRecord) recordList.get(i);
dataRecord.setRecordId(i + 1);
}
if (record instanceof LinkableRecord) {
LinkableRecord linkableRecord = (LinkableRecord) record;
uriSet.remove(linkableRecord.getURI());
uriSet.remove(linkableRecord.getAlternateURI());
}
}
private class CategoryRecord extends DataRecord {
protected int getType() {
return DataRecord.CATEGORY;
}
protected Paragraph[] writeData(PdbOutputStream out)
throws IOException {
for (int i = 0; i < categories.length; i++) {
String category = categories[i];
out.writeString(category);
out.writeByte(0);
}
return null;
}
}
private class Index implements Record {
public static final int HOME = 0;
public static final int EXTERNAL_BOOKMARKS = 1;
public static final int URL_HANDLING = 2;
public static final int DEFAULT_CATEGORY = 3;
public static final int ADDITIONAL_METADATA = 4;
public int getRecordId() {
return 1;
}
public void write(PdbOutputStream out) throws IOException {
ByteArrayOutputStream reservedRecordsStream = new ByteArrayOutputStream();
DataOutputStream data = new DataOutputStream(reservedRecordsStream);
data.writeShort(HOME);
data.writeShort(homeRecord.getRecordId());
DataRecord record = findDataRecord(URIIndexRecord.class);
if (record != null) {
data.writeShort(URL_HANDLING);
data.writeShort(record.getRecordId());
}
record = findDataRecord(CategoryRecord.class);
if (record != null) {
data.writeShort(DEFAULT_CATEGORY);
data.writeShort(record.getRecordId());
}
record = findDataRecord(MetadataRecord.class);
if (record != null) {
data.writeShort(ADDITIONAL_METADATA);
data.writeShort(record.getRecordId());
}
record = findDataRecord(BookmarkRecord.class);
if (record != null) {
data.writeShort(EXTERNAL_BOOKMARKS);
data.writeShort(record.getRecordId());
}
byte[] reservedRecords = reservedRecordsStream.toByteArray();
out.writeShort(1); // ID of Index Record, should be 1
out.writeShort(((compression == 2) ? 2 : 1));
out.writeShort(reservedRecords.length / 4); // Number of reserved records
out.write(reservedRecords);
}
}
private class MetadataRecord extends DataRecord {
protected int getType() {
return DataRecord.METADATA;
}
protected Paragraph[] writeData(PdbOutputStream out)
throws IOException {
int recordCount = 0;
recordCount += ((author != null) ? 1 : 0);
recordCount += ((title != null) ? 1 : 0);
recordCount += ((publicationDate != null) ? 1 : 0);
/*if (outputEncoding.equalsIgnoreCase("UTF-8")) {
recordCount++;
}*/
out.writeShort(recordCount);
/*if (outputEncoding.equalsIgnoreCase("UTF-8")) {
System.err.println("Writing UTF-8 charset.");
out.writeShort(1);
out.writeShort(1);
out.writeShort(106);
}*/
if (author != null) {
out.writeShort(4);
writeText(out, author);
}
if (title != null) {
out.writeShort(5);
writeText(out, title);
}
if (publicationDate != null) {
out.writeShort(6);
out.writeShort(2);
out.writeDate(publicationDate);
}
return null;
}
private void writeText(PdbOutputStream out, String text)
throws IOException {
int length = text.length() + 1;
out.writeShort((length + (length % 1)) / 2);
out.writeString(text);
out.write(0);
if ((length % 1) == 1) {
out.write(0);
}
}
}
/**
* This class represents what the Plucker format specification calls a "URL data record"
*/
private class URIDataRecord extends DataRecord {
private String[] uris;
private int lastRecordId;
URIDataRecord(String[] uris, int lastRecordId) {
this.uris = uris;
this.lastRecordId = lastRecordId;
}
public int getLastRecordId() {
return lastRecordId;
}
protected int getType() {
return DataRecord.LINKS;
}
protected Paragraph[] writeData(PdbOutputStream out)
throws IOException {
for (int i = 0; i < uris.length; i++) {
String uri = uris[i];
if (uri != null) {
out.writeString(uri);
}
out.writeByte(0);
}
return null;
}
}
/**
* This class represents what the Plucker format specification calls a "URL handling record"
*/
private class URIIndexRecord extends DataRecord {
private URIDataRecord[] records;
public URIIndexRecord(URIDataRecord[] records) {
this.records = records;
}
protected int getType() {
return DataRecord.LINK_INDEX;
}
protected Paragraph[] writeData(PdbOutputStream out)
throws IOException {
for (int i = 0; i < records.length; i++) {
URIDataRecord record = records[i];
out.writeShort(record.getLastRecordId());
out.writeShort(record.getRecordId());
}
return null;
}
}
private List bookmarkList = new ArrayList();
public void addBookmark(String name, String uri) {
addBookmark(new Bookmark(name, uri));
}
public void addBookmark(Bookmark bookmark) {
if (!bookmarkList.contains(bookmark)) {
bookmarkList.add(bookmark);
}
}
public boolean containsBookmark(String uri) {
boolean contains = false;
for (Iterator it = bookmarkList.iterator(); it.hasNext(); ) {
Bookmark bookmark = (Bookmark)it.next();
if (bookmark.getURI().equals(uri)) {
contains = true;
break;
}
}
return contains;
}
private class BookmarkRecord extends DataRecord {
protected int getType() {
return DataRecord.BOOKMARKS;
}
protected Paragraph[] writeData(PdbOutputStream out) throws IOException {
List bookmarks = new ArrayList();
List links = new ArrayList();
bookmarks.addAll(bookmarkList);
int offset = 12;
for (int i = 0; i < bookmarks.size(); i++) {
Bookmark bookmark = (Bookmark)bookmarks.get(i);
Link link = resolve(bookmark.getURI());
if (link != null) {
offset += bookmark.getName().length() + 1;
links.add(link);
} else {
bookmarks.remove(i);
}
}
out.writeShort(bookmarks.size());
out.writeShort(offset);
for (Iterator it = bookmarks.iterator(); it.hasNext();) {
Bookmark bookmark = (Bookmark) it.next();
out.writeNullTerminatedString(bookmark.getName());
}
for (Iterator it = links.iterator(); it.hasNext();) {
Link link = (Link) it.next();
out.writeShort(link.getRecordId());
int paragraph = link.getParagraph();
if (paragraph == -1) {
paragraph = 0;
}
out.writeShort(paragraph);
}
return null;
}
}
public void setFilename(String filename) {
this.filename = filename;
}
public String getFilename() {
String s;
if (this.filename !=null) {
s = this.filename;
if (!s.toLowerCase().endsWith(".pdb")) {
s += ".pdb";
}
}
else {
s =Database.convertToFilename(this.name) + ".pdb";
}
return s;
}
public void sortBookmarks() {
Collections.sort(bookmarkList);
}
}