//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); } }