//wikiBoard.java
//-------------------------------------
//(C) by Michael Peter Christen; mc@yacy.net
//first published on http://www.anomic.de
//Frankfurt, Germany, 2004
//
// $LastChangedDate$
// $LastChangedRevision$
// $LastChangedBy$
//
//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 net.yacy.data.wiki;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import net.yacy.cora.document.encoding.ASCII;
import net.yacy.cora.document.encoding.UTF8;
import net.yacy.cora.order.Base64Order;
import net.yacy.cora.order.NaturalOrder;
import net.yacy.cora.protocol.Domains;
import net.yacy.cora.util.CommonPattern;
import net.yacy.cora.util.ConcurrentLog;
import net.yacy.cora.util.SpaceExceededException;
import net.yacy.kelondro.blob.MapHeap;
/**
*
*/
public class WikiBoard {
public static final int keyLength = 64;
private static final String DATE_FORMAT = "yyyyMMddHHmmss";
private static final String ANONYMOUS = "anonymous";
protected static final SimpleDateFormat SimpleFormatter = new SimpleDateFormat(DATE_FORMAT, Locale.US);
static {
SimpleFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));
}
private MapHeap datbase = null;
private MapHeap bkpbase = null;
private static final Map<String, String> AUTHORS = new HashMap<String, String>();
/**
* Constructor.
* @param actpath path of database which contains current wiki data.
* @param bkppath path of backup database.
* @throws IOException if error occurs during HDD access.
*/
public WikiBoard(final File actpath, final File bkppath) throws IOException {
new File(actpath.getParent()).mkdirs();
if (this.datbase == null) {
//datbase = new MapView(BLOBTree.toHeap(actpath, true, true, keyLength, recordSize, '_', NaturalOrder.naturalOrder, actpathNew), 500, '_');
this.datbase = new MapHeap(actpath, keyLength, NaturalOrder.naturalOrder, 1024 * 64, 500, '_');
}
new File(bkppath.getParent()).mkdirs();
if (this.bkpbase == null) {
//bkpbase = new MapView(BLOBTree.toHeap(bkppath, true, true, keyLength + dateFormat.length(), recordSize, '_', NaturalOrder.naturalOrder, bkppathNew), 500, '_');
this.bkpbase = new MapHeap(bkppath, keyLength + DATE_FORMAT.length(), NaturalOrder.naturalOrder, 1024 * 64, 500, '_');
}
}
/**
* Gets total number of entries of wiki DB and DB which contains backup entries.
* @return number of entries in wiki plus number of old entries.
*/
public int sizeOfTwo() {
return this.datbase.size() + this.bkpbase.size();
}
/**
* Gets number of entries of wiki DB.
* @return number of entries in wiki.
*/
public int size() {
return this.datbase.size();
}
/**
* Closes database files.
*/
public synchronized void close() {
this.datbase.close();
this.bkpbase.close();
}
/**
* Gets current date.
* @return current date.
*/
static String dateString() {
return dateString(new Date());
}
/**
* Gets String representation of a Date.
* @param date the Date.
* @return String representation of Date.
*/
public static String dateString(final Date date) {
synchronized (SimpleFormatter) {
return SimpleFormatter.format(date);
}
}
/**
* Gets normalized version of a key.
* @param key the key.
* @return normalized version of key.
*/
private static String normalize(final String key) {
return (key != null) ? key.trim().toLowerCase() : "null";
}
/**
* Normalizes key and replaces spaces by escape code.
* @param key the key.
* @return normalized and webalized version.
*/
public static String webalize(final String key) {
return (key != null) ? CommonPattern.SPACE.matcher(normalize(key)).replaceAll("%20") : "null";
}
/**
* Tries to guess the name of the author by a given IP address.
* @param ip the IP address.
* @return
*/
public static String guessAuthor(final String ip) {
final String author = AUTHORS.get(ip);
//yacyCore.log.logDebug("DEBUG: guessing author for ip = " + ip + " is '" + author + "', authors = " + authors.toString());
return author;
}
/**
* Adds an author name and a corresponding IP to internal Map.
* @param ip IP address of the author.
* @param author name of author.
*/
public static void setAuthor(final String ip, final String author) {
AUTHORS.put(ip,author);
}
/**
* Creates new Entry.
* @param subject subject of entry.
* @param author author of entry.
* @param ip IP address of author.
* @param reason reason for new Entry (for example "edit").
* @param page content of Entry.
* @return new Entry.
* @throws IOException
*/
public Entry newEntry(final String subject, final String author, final String ip, final String reason, final byte[] page) throws IOException {
return new Entry(normalize(subject), author, ip, reason, page);
}
/**
* Contains information of wiki page.
*/
public class Entry {
private static final String ANONYMOUS = "anonymous";
private final String key;
private final Map<String, String> record;
/**
* Constructor which creates new Entry using given information.
* @param subject subject of Entry.
* @param author author of Entry.
* @param ip IP address of author.
* @param reason reason for new Entry (for example "edit").
* @param page content of Entry.
* @throws IOException
*/
public Entry(final String subject, final String author, final String ip, final String reason, final byte[] page) throws IOException {
this.record = new HashMap<String, String>();
this.key = subject.substring(0, Math.min((subject != null) ? subject.length() : 0, keyLength));
this.record.put("date", dateString());
this.record.put("author", Base64Order.enhancedCoder.encodeString((author != null && author.length() > 0) ? author : ANONYMOUS));
this.record.put("ip", (ip != null && ip.length() > 0) ? ip : "");
this.record.put("reason", Base64Order.enhancedCoder.encodeString((reason != null && reason.length() > 0) ? reason : ""));
this.record.put("page", (page != null) ? Base64Order.enhancedCoder.encode(page) : "");
AUTHORS.put(ip, author);
}
/**
* Constructor which creates Entry using key and record.
* @param key key of Entry.
* @param record record which contains data.
*/
Entry(final String key, final Map<String, String> record) {
this.key = key;
this.record = record;
}
/**
* Gets subject of Entry.
* @return subject of entry.
*/
public String subject() {
return this.key;
}
/**
* Gets date of Entry.
* @return date of Entry.
*/
public Date date() {
Date ret;
try {
final String c = this.record.get("date");
if (c == null) {
System.out.println("DEBUG - ERROR: date field missing in wikiBoard");
ret = new Date();
} else {
synchronized (SimpleFormatter) {
ret = SimpleFormatter.parse(c);
}
}
} catch (final ParseException e) {
ret = new Date();
}
return ret;
}
/**
* Gets author of Entry.
* @return author of Entry.
*/
public String author() {
final String a = this.record.get("author");
final byte[] b;
return (a != null && (b = Base64Order.enhancedCoder.decode(a)) != null) ? UTF8.String(b) : ANONYMOUS;
}
/**
* Gets reason for Entry.
* @return reason for Entry.
*/
public String reason() {
final String ret;
final String r = this.record.get("reason");
if (r != null) {
final byte[] b;
ret = ((b = Base64Order.enhancedCoder.decode(r)) != null) ? UTF8.String(b) : "unknown";
} else {
ret = "";
}
return ret;
}
/**
* Gets actual content of Entry.
* @return content of Entry.
*/
public byte[] page() {
final String m = this.record.get("page");
final byte[] b;
return (m != null && (b = Base64Order.enhancedCoder.decode(m)) != null) ? b : new byte[0];
}
/**
* Sets date of previous version of Entry.
* @param date date of previous version of Entry.
*/
void setAncestorDate(final Date date) {
this.record.put("bkp", dateString(date));
}
/**
* Gets date of previous version of Entry.
* @return date of previous version of Entry.
*/
private Date getAncestorDate() {
Date ret = null;
try {
final String c = this.record.get("date");
if (c != null) {
synchronized (SimpleFormatter) {
ret = SimpleFormatter.parse(c);
}
}
} catch (final ParseException e) {
ret = null;
}
return ret;
}
/**
* Gets previous version of Entry.
* @return previous version of Entry.
*/
public Entry getAncestor() {
final Date ancDate = getAncestorDate();
return (ancDate == null) ? null : read(this.key + dateString(ancDate), WikiBoard.this.bkpbase);
}
/**
* Adds child of current Entry.
* @param subject subject of child of current Entry.
*/
void setChild(final String subject) {
this.record.put("child", Base64Order.enhancedCoder.encode(UTF8.getBytes(subject)));
}
/**
* Gets name (= subject) of child of this Entry.
* @return name of child of this Entry.
*/
private String getChildName() {
final String c = this.record.get("child");
final byte[] subject;
return (c != null && (subject = Base64Order.enhancedCoder.decode(c)) != null) ? ASCII.String(subject) : null;
}
/**
* Tells if Entry has child.
* @return true if has child, else false.
*/
public boolean hasChild() {
final String c = this.record.get("child");
return (c != null && Base64Order.enhancedCoder.decode(c) != null) ? true : false;
}
/**
* Gets child of this Entry.
* @return child of this Entry.
*/
public Entry getChild() {
final String childName = getChildName();
return (childName == null) ? null : read(childName, WikiBoard.this.datbase);
}
}
/**
* Writes content of Entry to database and returns key.
* @param entry Entry to be written.
* @return key of Entry.
*/
public String write(final Entry entry) {
// writes a new page and returns key
String key = null;
try {
// first load the old page
final Entry oldEntry = read(entry.key);
// set the bkp date of the new page to the date of the old page
final Date oldDate = oldEntry.date();
entry.setAncestorDate(oldDate);
oldEntry.setChild(entry.subject());
// write the backup
this.bkpbase.insert(UTF8.getBytes(entry.key + dateString(oldDate)), oldEntry.record);
// write the new page
this.datbase.insert(UTF8.getBytes(entry.key), entry.record);
key = entry.key;
} catch (final Exception e) {
ConcurrentLog.logException(e);
}
return key;
}
/**
* Reads content of Entry from database.
* @param key key of Entry.
* @return Entry which contains data.
*/
public Entry read(final String key) {
return read(key, this.datbase);
}
/**
* Reads content of Entry from database.
* @param key key of Entry.
* @param base database containing data.
* @return Entry which contains data.
*/
Entry read(final String key, final MapHeap base) {
Entry ret = null;
try {
String copyOfKey = normalize(key);
if (copyOfKey.length() > keyLength) {
copyOfKey = copyOfKey.substring(0, keyLength);
}
final Map<String, String> record = base.get(UTF8.getBytes(copyOfKey));
ret = (record == null) ? newEntry(copyOfKey, ANONYMOUS, Domains.LOCALHOST, "New Page", UTF8.getBytes("")) : new Entry(copyOfKey, record);
} catch (final IOException e) {
ConcurrentLog.logException(e);
} catch (final SpaceExceededException e) {
ConcurrentLog.logException(e);
}
return ret;
}
/**
* Reads old Entry from backup database.
* @param key key of Entry.
* @return the Entry.
*/
public Entry readBkp(final String key) {
return read(key, this.bkpbase);
}
/**
* Gets Iterator of keys in database.
* @param up
* @return keys of Entries in database.
* @throws IOException
*/
public Iterator<byte[]> keys(final boolean up) throws IOException {
return this.datbase.keys(up, false);
}
/**
* Gets Iterator of keys in backup database.
* @param up
* @return keys of Entries in backup database.
* @throws IOException
*/
public Iterator<byte[]> keysBkp(final boolean up) throws IOException {
return this.bkpbase.keys(up, false);
}
}