You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by aj...@apache.org on 2008/02/13 06:54:24 UTC
svn commit: r627255 [3/41] - in
/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src: ./ com/ com/ecyrd/
com/ecyrd/jspwiki/ com/ecyrd/jspwiki/action/ com/ecyrd/jspwiki/attachment/
com/ecyrd/jspwiki/auth/ com/ecyrd/jspwiki/auth/acl/ com/ecyrd/jspwiki/...
Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/ReferenceManager.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/ReferenceManager.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/ReferenceManager.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/ReferenceManager.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,1133 @@
+/*
+ JSPWiki - a JSP-based WikiWiki clone.
+
+ Copyright (C) 2001-2004 Janne Jalkanen (Janne.Jalkanen@iki.fi),
+ Erik Bunn (ebu@memecry.net)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser 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.ecyrd.jspwiki;
+
+import java.io.*;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+
+import org.apache.commons.lang.time.StopWatch;
+import org.apache.log4j.Logger;
+
+import com.ecyrd.jspwiki.attachment.Attachment;
+import com.ecyrd.jspwiki.event.WikiEvent;
+import com.ecyrd.jspwiki.event.WikiEventListener;
+import com.ecyrd.jspwiki.event.WikiEventUtils;
+import com.ecyrd.jspwiki.event.WikiPageEvent;
+import com.ecyrd.jspwiki.filters.BasicPageFilter;
+import com.ecyrd.jspwiki.modules.InternalModule;
+import com.ecyrd.jspwiki.providers.ProviderException;
+import com.ecyrd.jspwiki.providers.WikiPageProvider;
+
+/*
+ BUGS
+
+ - if a wikilink is added to a page, then removed, RefMan still thinks that
+ the page refers to the wikilink page. Hm.
+
+ - if a page is deleted, gets very confused.
+
+ - Serialization causes page attributes to be missing, when InitializablePlugins
+ are not executed properly. Thus, serialization should really also mark whether
+ a page is serializable or not...
+ */
+
+
+/*
+ A word about synchronizing:
+
+ I expect this object to be accessed in three situations:
+ - when a WikiEngine is created and it scans its wikipages
+ - when the WE saves a page
+ - when a JSP page accesses one of the WE's ReferenceManagers
+ to display a list of (un)referenced pages.
+
+ So, access to this class is fairly rare, and usually triggered by
+ user interaction. OTOH, the methods in this class use their storage
+ objects intensively (and, sorry to say, in an unoptimized manner =).
+ My deduction: using unsynchronized HashMaps etc and syncing methods
+ or code blocks is preferrable to using slow, synced storage objects.
+ We don't have iterative code here, so I'm going to use synced methods
+ for now.
+
+ Please contact me if you notice problems with ReferenceManager, and
+ especially with synchronization, or if you have suggestions about
+ syncing.
+
+ ebu@memecry.net
+*/
+
+/**
+ * Keeps track of wikipage references:
+ * <UL>
+ * <LI>What pages a given page refers to
+ * <LI>What pages refer to a given page
+ * </UL>
+ *
+ * This is a quick'n'dirty approach without any finesse in storage and
+ * searching algorithms; we trust java.util.*.
+ * <P>
+ * This class contains two HashMaps, m_refersTo and m_referredBy. The
+ * first is indexed by WikiPage names and contains a Collection of all
+ * WikiPages the page refers to. (Multiple references are not counted,
+ * naturally.) The second is indexed by WikiPage names and contains
+ * a Set of all pages that refer to the indexing page. (Notice -
+ * the keys of both Maps should be kept in sync.)
+ * <P>
+ * When a page is added or edited, its references are parsed, a Collection
+ * is received, and we crudely replace anything previous with this new
+ * Collection. We then check each referenced page name and make sure they
+ * know they are referred to by the new page.
+ * <P>
+ * Based on this information, we can perform non-optimal searches for
+ * e.g. unreferenced pages, top ten lists, etc.
+ * <P>
+ * The owning class must take responsibility of filling in any pre-existing
+ * information, probably by loading each and every WikiPage and calling this
+ * class to update the references when created.
+ *
+ * @author ebu@memecry.net
+ * @since 1.6.1
+ */
+
+// FIXME: The way that we save attributes is now a major booboo, and must be
+// replace forthwith. However, this is a workaround for the great deal
+// of problems that occur here...
+
+public class ReferenceManager
+ extends BasicPageFilter
+ implements InternalModule, WikiEventListener
+{
+ /** Maps page wikiname to a Collection of pages it refers to. The Collection
+ * must contain Strings. The Collection may contain names of non-existing
+ * pages.
+ */
+ private Map<String,Set<String>> m_refersTo;
+ private Map<String,Set<String>> m_unmutableRefersTo;
+
+ /** Maps page wikiname to a Set of referring pages. The Set must
+ * contain Strings. Non-existing pages (a reference exists, but not a file
+ * for the page contents) may have an empty Set in m_referredBy.
+ */
+ private Map<String,Set<String>> m_referredBy;
+ private Map<String,Set<String>> m_unmutableReferredBy;
+
+ /** The WikiEngine that owns this object. */
+ private WikiEngine m_engine;
+
+ private boolean m_matchEnglishPlurals = false;
+
+ private static Logger log = Logger.getLogger(ReferenceManager.class);
+
+ private static final String SERIALIZATION_FILE = "refmgr.ser";
+ private static final String SERIALIZATION_DIR = "refmgr-attr";
+
+ /** We use this also a generic serialization id */
+ private static final long serialVersionUID = 2L;
+
+ /**
+ * Builds a new ReferenceManager.
+ *
+ * @param engine The WikiEngine to which this is managing references to.
+ */
+ public ReferenceManager( WikiEngine engine )
+ {
+ m_refersTo = new HashMap<String,Set<String>>();
+ m_referredBy = new HashMap<String,Set<String>>();
+ m_engine = engine;
+
+ m_matchEnglishPlurals = TextUtil.getBooleanProperty( engine.getWikiProperties(),
+ WikiEngine.PROP_MATCHPLURALS,
+ m_matchEnglishPlurals );
+
+ //
+ // Create two maps that contain unmutable versions of the two basic maps.
+ //
+ m_unmutableReferredBy = Collections.unmodifiableMap( m_referredBy );
+ m_unmutableRefersTo = Collections.unmodifiableMap( m_refersTo );
+ }
+
+ /**
+ * Does a full reference update. Does not sync; assumes that you do it afterwards.
+ */
+ private void updatePageReferences( WikiPage page )
+ throws ProviderException
+ {
+ String content = m_engine.getPageManager().getPageText( page.getName(),
+ WikiPageProvider.LATEST_VERSION );
+
+ TreeSet<String> res = new TreeSet<String>();
+ Collection<String> links = m_engine.scanWikiLinks( page, content );
+
+ res.addAll( links );
+ Collection<Attachment> attachments = m_engine.getAttachmentManager().listAttachments( page );
+
+ for( Attachment atti : attachments )
+ {
+ res.add( atti.getName() );
+ }
+
+ internalUpdateReferences( page.getName(), res );
+ }
+
+ /**
+ * Initializes the entire reference manager with the initial set of pages
+ * from the collection.
+ *
+ * @param pages A collection of all pages you want to be included in the reference
+ * count.
+ * @since 2.2
+ */
+ public void initialize( Collection<WikiPage> pages )
+ throws ProviderException
+ {
+ log.debug( "Initializing new ReferenceManager with "+pages.size()+" initial pages." );
+ StopWatch sw = new StopWatch();
+ sw.start();
+ log.info( "Starting cross reference scan of WikiPages" );
+
+ //
+ // First, try to serialize old data from disk. If that fails,
+ // we'll go and update the entire reference lists (which'll take
+ // time)
+ //
+ try
+ {
+ //
+ // Unserialize things. The loop below cannot be combined with
+ // the other loop below, simply because engine.getPage() has
+ // side effects such as loading initializing the user databases,
+ // which in turn want all of the pages to be read already...
+ //
+ // Yes, this is a kludge. We know. Will be fixed.
+ //
+ long saved = unserializeFromDisk();
+
+ for( WikiPage page : pages )
+ {
+ unserializeAttrsFromDisk( page );
+ }
+
+ //
+ // Now we must check if any of the pages have been changed
+ // while we were in the electronic la-la-land, and update
+ // the references for them.
+ //
+
+ Iterator<WikiPage> it = pages.iterator();
+
+ while( it.hasNext() )
+ {
+ WikiPage page = it.next();
+
+ if( page instanceof Attachment )
+ {
+ // Skip attachments
+ }
+ else
+ {
+
+ // Refresh with the latest copy
+ page = m_engine.getPage( page.getName() );
+
+ if( page.getLastModified() == null )
+ {
+ log.fatal( "Provider returns null lastModified. Please submit a bug report." );
+ }
+ else if( page.getLastModified().getTime() > saved )
+ {
+ updatePageReferences( page );
+ }
+ }
+ }
+
+ }
+ catch( Exception e )
+ {
+ log.info("Unable to unserialize old refmgr information, rebuilding database: "+e.getMessage());
+ buildKeyLists( pages );
+
+ // Scan the existing pages from disk and update references in the manager.
+ Iterator<WikiPage> it = pages.iterator();
+ while( it.hasNext() )
+ {
+ WikiPage page = it.next();
+
+ if( page instanceof Attachment )
+ {
+ // We cannot build a reference list from the contents
+ // of attachments, so we skip them.
+ }
+ else
+ {
+ updatePageReferences( page );
+
+ serializeAttrsToDisk( page );
+ }
+
+ }
+
+ serializeToDisk();
+ }
+
+ sw.stop();
+ log.info( "Cross reference scan done in "+sw );
+
+ WikiEventUtils.addWikiEventListener(m_engine.getPageManager(),
+ WikiPageEvent.PAGE_DELETED, this);
+ }
+
+ /**
+ * Reads the serialized data from the disk back to memory.
+ * Returns the date when the data was last written on disk
+ */
+ private synchronized long unserializeFromDisk()
+ throws IOException,
+ ClassNotFoundException
+ {
+ ObjectInputStream in = null;
+ long saved = 0L;
+
+ try
+ {
+ StopWatch sw = new StopWatch();
+ sw.start();
+
+ File f = new File( m_engine.getWorkDir(), SERIALIZATION_FILE );
+
+ in = new ObjectInputStream( new BufferedInputStream(new FileInputStream(f)) );
+
+ long ver = in.readLong();
+
+ if( ver != serialVersionUID )
+ {
+ throw new IOException("File format has changed; I need to recalculate references.");
+ }
+
+ saved = in.readLong();
+ m_refersTo = (Map<String,Set<String>>) in.readObject();
+ m_referredBy = (Map<String,Set<String>>) in.readObject();
+
+ in.close();
+
+ m_unmutableReferredBy = Collections.unmodifiableMap( m_referredBy );
+ m_unmutableRefersTo = Collections.unmodifiableMap( m_refersTo );
+
+ sw.stop();
+ log.debug("Read serialized data successfully in "+sw);
+ }
+ finally
+ {
+ try
+ {
+ if( in != null ) in.close();
+ }
+ catch( IOException ex ) {}
+ }
+
+ return saved;
+ }
+
+ /**
+ * Serializes hashmaps to disk. The format is private, don't touch it.
+ */
+ private synchronized void serializeToDisk()
+ {
+ ObjectOutputStream out = null;
+
+ try
+ {
+ StopWatch sw = new StopWatch();
+ sw.start();
+
+ File f = new File( m_engine.getWorkDir(), SERIALIZATION_FILE );
+
+ out = new ObjectOutputStream( new BufferedOutputStream(new FileOutputStream(f)) );
+
+ out.writeLong( serialVersionUID );
+ out.writeLong( System.currentTimeMillis() ); // Timestamp
+ out.writeObject( m_refersTo );
+ out.writeObject( m_referredBy );
+
+ out.close();
+
+ sw.stop();
+
+ log.debug("serialization done - took "+sw);
+ }
+ catch( IOException e )
+ {
+ log.error("Unable to serialize!");
+
+ try
+ {
+ if( out != null ) out.close();
+ }
+ catch( IOException ex ) {}
+ }
+ }
+
+ private String getHashFileName( String pageName )
+ throws NoSuchAlgorithmException
+ {
+ MessageDigest digest = MessageDigest.getInstance("MD5");
+
+ byte[] dig;
+ try
+ {
+ dig = digest.digest( pageName.getBytes("UTF-8") );
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ throw new InternalWikiException("AAAAGH! UTF-8 is gone! My eyes! It burns...!");
+ }
+
+ return TextUtil.toHexString(dig)+".cache";
+ }
+
+ /**
+ * Reads the serialized data from the disk back to memory.
+ * Returns the date when the data was last written on disk
+ */
+ private synchronized long unserializeAttrsFromDisk(WikiPage p)
+ throws IOException,
+ ClassNotFoundException
+ {
+ ObjectInputStream in = null;
+ long saved = 0L;
+
+ try
+ {
+ StopWatch sw = new StopWatch();
+ sw.start();
+
+ //
+ // Find attribute cache, and check if it exists
+ //
+ File f = new File( m_engine.getWorkDir(), SERIALIZATION_DIR );
+
+ f = new File( f, getHashFileName(p.getName()) );
+
+ if( !f.exists() )
+ {
+ return 0L;
+ }
+
+ log.debug("Deserializing attributes for "+p.getName());
+
+ in = new ObjectInputStream( new BufferedInputStream(new FileInputStream(f)) );
+
+ long ver = in.readLong();
+
+ if( ver != serialVersionUID )
+ {
+ log.debug("File format has changed; cannot deserialize.");
+ return 0L;
+ }
+
+ saved = in.readLong();
+
+ String name = in.readUTF();
+
+ if( !name.equals(p.getName()) )
+ {
+ log.debug("File name does not match ("+name+"), skipping...");
+ return 0L; // Not here
+ }
+
+ long entries = in.readLong();
+
+ for( int i = 0; i < entries; i++ )
+ {
+ String key = in.readUTF();
+ Object value = in.readObject();
+
+ p.setAttribute( key, value );
+
+ log.debug(" attr: "+key+"="+value);
+ }
+
+ in.close();
+
+ sw.stop();
+ log.debug("Read serialized data for "+name+" successfully in "+sw);
+ }
+ catch( NoSuchAlgorithmException e )
+ {
+ log.fatal("No MD5!?!");
+ }
+ finally
+ {
+ try
+ {
+ if( in != null ) in.close();
+ }
+ catch( IOException ex ) {}
+ }
+
+ return saved;
+ }
+
+ /**
+ * Serializes hashmaps to disk. The format is private, don't touch it.
+ */
+ private synchronized void serializeAttrsToDisk( WikiPage p )
+ {
+ ObjectOutputStream out = null;
+
+ try
+ {
+ // FIXME: There is a concurrency issue here...
+ Set entries = p.getAttributes().entrySet();
+
+ if( entries.size() == 0 ) return;
+
+ StopWatch sw = new StopWatch();
+ sw.start();
+
+ File f = new File( m_engine.getWorkDir(), SERIALIZATION_DIR );
+
+ if( !f.exists() ) f.mkdirs();
+
+ //
+ // Create a digest for the name
+ //
+ f = new File( f, getHashFileName(p.getName()) );
+
+ out = new ObjectOutputStream( new BufferedOutputStream(new FileOutputStream(f)) );
+
+ out.writeLong( serialVersionUID );
+ out.writeLong( System.currentTimeMillis() ); // Timestamp
+
+ out.writeUTF( p.getName() );
+ out.writeLong( entries.size() );
+
+ for( Iterator i = entries.iterator(); i.hasNext(); )
+ {
+ Map.Entry e = (Map.Entry) i.next();
+
+ if( e.getValue() instanceof Serializable )
+ {
+ out.writeUTF( (String)e.getKey() );
+ out.writeObject( e.getValue() );
+ }
+ }
+
+ out.close();
+
+ sw.stop();
+
+ log.debug("serialization for "+p.getName()+" done - took "+sw);
+ }
+ catch( IOException e )
+ {
+ log.error("Unable to serialize!");
+
+ try
+ {
+ if( out != null ) out.close();
+ }
+ catch( IOException ex ) {}
+ }
+ catch( NoSuchAlgorithmException e )
+ {
+ log.fatal("No MD5 algorithm!?!");
+ }
+ }
+
+ /**
+ * After the page has been saved, updates the reference lists.
+ */
+ public void postSave( WikiContext context, String content )
+ {
+ WikiPage page = context.getPage();
+
+ updateReferences( page.getName(),
+ context.getEngine().scanWikiLinks( page, content ) );
+
+ serializeAttrsToDisk( page );
+ }
+
+ /**
+ * Updates the m_referedTo and m_referredBy hashmaps when a page has been
+ * deleted.
+ * <P>
+ * Within the m_refersTo map the pagename is a key. The whole key-value-set
+ * has to be removed to keep the map clean.
+ * Within the m_referredBy map the name is stored as a value. Since a key
+ * can have more than one value we have to delete just the key-value-pair
+ * referring page:deleted page.
+ *
+ * @param page Name of the page to remove from the maps.
+ */
+ public synchronized void pageRemoved( WikiPage page )
+ {
+ String pageName = page.getName();
+
+ pageRemoved(pageName);
+ }
+
+ private void pageRemoved(String pageName)
+ {
+ Collection<String> refTo = m_refersTo.get( pageName );
+
+ if( refTo != null )
+ {
+ Iterator it_refTo = refTo.iterator();
+ while( it_refTo.hasNext() )
+ {
+ String referredPageName = (String)it_refTo.next();
+ Set<String> refBy = m_referredBy.get( referredPageName );
+
+ if( refBy == null )
+ throw new InternalWikiException("Refmgr out of sync: page "+pageName+" refers to "+referredPageName+", which has null referrers.");
+
+ refBy.remove(pageName);
+
+ m_referredBy.remove( referredPageName );
+
+ // We won't put it back again if it becomes empty and does not exist. It will be added
+ // later on anyway, if it becomes referenced again.
+ if( !(refBy.isEmpty() && !m_engine.pageExists(referredPageName)) )
+ {
+ m_referredBy.put( referredPageName, refBy );
+ }
+ }
+
+ log.debug("Removing from m_refersTo HashMap key:value "+pageName+":"+m_refersTo.get( pageName ));
+ m_refersTo.remove( pageName );
+ }
+
+ Set<String> refBy = m_referredBy.get( pageName );
+ if( refBy == null || refBy.isEmpty() )
+ {
+ m_referredBy.remove( pageName );
+ }
+
+ //
+ // Remove any traces from the disk, too
+ //
+ serializeToDisk();
+
+ try
+ {
+ File f = new File( m_engine.getWorkDir(), SERIALIZATION_DIR );
+
+ f = new File( f, getHashFileName(pageName) );
+
+ if( f.exists() ) f.delete();
+ }
+ catch( NoSuchAlgorithmException e )
+ {
+ log.error("What do you mean - no such algorithm?", e);
+ }
+ }
+
+ /**
+ * Updates the referred pages of a new or edited WikiPage. If a refersTo
+ * entry for this page already exists, it is removed and a new one is built
+ * from scratch. Also calls updateReferredBy() for each referenced page.
+ * <P>
+ * This is the method to call when a new page has been created and we
+ * want to a) set up its references and b) notify the referred pages
+ * of the references. Use this method during run-time.
+ *
+ * @param page Name of the page to update.
+ * @param references A Collection of Strings, each one pointing to a page this page references.
+ */
+ public synchronized void updateReferences( String page, Collection<String> references )
+ {
+ internalUpdateReferences(page, references);
+
+ serializeToDisk();
+ }
+
+ /**
+ * Updates the referred pages of a new or edited WikiPage. If a refersTo
+ * entry for this page already exists, it is removed and a new one is built
+ * from scratch. Also calls updateReferredBy() for each referenced page.
+ * <p>
+ * This method does not synchronize the database to disk.
+ *
+ * @param page Name of the page to update.
+ * @param references A Collection of Strings, each one pointing to a page this page references.
+ */
+
+ private void internalUpdateReferences(String page, Collection<String> references)
+ {
+ page = getFinalPageName( page );
+
+ //
+ // Create a new entry in m_refersTo.
+ //
+ Collection<String> oldRefTo = m_refersTo.get( page );
+ m_refersTo.remove( page );
+
+ TreeSet<String> cleanedRefs = new TreeSet<String>();
+ for( String ref : references )
+ {
+ ref = getFinalPageName( ref );
+
+ cleanedRefs.add( ref );
+ }
+
+ m_refersTo.put( page, cleanedRefs );
+
+ //
+ // We know the page exists, since it's making references somewhere.
+ // If an entry for it didn't exist previously in m_referredBy, make
+ // sure one is added now.
+ //
+ if( !m_referredBy.containsKey( page ) )
+ {
+ m_referredBy.put( page, new TreeSet<String>() );
+ }
+
+ //
+ // Get all pages that used to be referred to by 'page' and
+ // remove that reference. (We don't want to try to figure out
+ // which particular references were removed...)
+ //
+ cleanReferredBy( page, oldRefTo, cleanedRefs );
+
+ //
+ // Notify all referred pages of their referinesshoodicity.
+ //
+ for( String referredPageName : cleanedRefs )
+ {
+ updateReferredBy( getFinalPageName(referredPageName), page );
+ }
+ }
+
+ /**
+ * Returns the refers-to list. For debugging.
+ */
+ protected Map getRefersTo()
+ {
+ return m_refersTo;
+ }
+
+ /**
+ * Returns the referred-by list. For debugging.
+ */
+ protected Map getReferredBy()
+ {
+ return m_referredBy;
+ }
+
+ /**
+ * Cleans the 'referred by' list, removing references by 'referrer' to
+ * any other page. Called after 'referrer' is removed.
+ */
+ private void cleanReferredBy( String referrer,
+ Collection<String> oldReferred,
+ Collection<String> newReferred )
+ {
+ // Two ways to go about this. One is to look up all pages previously
+ // referred by referrer and remove referrer from their lists, and let
+ // the update put them back in (except possibly removed ones).
+ // The other is to get the old referred to list, compare to the new,
+ // and tell the ones missing in the latter to remove referrer from
+ // their list. Hm. We'll just try the first for now. Need to come
+ // back and optimize this a bit.
+
+ if( oldReferred == null )
+ return;
+
+ for( String referredPage : oldReferred )
+ {
+ Set<String> oldRefBy = m_referredBy.get( referredPage );
+ if( oldRefBy != null )
+ {
+ oldRefBy.remove( referrer );
+ }
+
+ // If the page is referred to by no one AND it doesn't even
+ // exist, we might just as well forget about this entry.
+ // It will be added again elsewhere if new references appear.
+ if( ( ( oldRefBy == null ) || ( oldRefBy.isEmpty() ) ) &&
+ ( m_engine.pageExists( referredPage ) == false ) )
+ {
+ m_referredBy.remove( referredPage );
+ }
+ }
+
+ }
+
+
+ /**
+ * When initially building a ReferenceManager from scratch, call this method
+ * BEFORE calling updateReferences() with a full list of existing page names.
+ * It builds the refersTo and referredBy key lists, thus enabling
+ * updateReferences() to function correctly.
+ * <P>
+ * This method should NEVER be called after initialization. It clears all mappings
+ * from the reference tables.
+ *
+ * @param pages a Collection containing WikiPage objects.
+ */
+ private synchronized void buildKeyLists( Collection<WikiPage> pages )
+ {
+ m_refersTo.clear();
+ m_referredBy.clear();
+
+ if( pages == null )
+ return;
+
+ try
+ {
+ for( WikiPage page : pages )
+ {
+ // We add a non-null entry to referredBy to indicate the referred page exists
+ m_referredBy.put( page.getName(), new TreeSet<String>() );
+ // Just add a key to refersTo; the keys need to be in sync with referredBy.
+ m_refersTo.put( page.getName(), (TreeSet<String>)null );
+ }
+ }
+ catch( ClassCastException e )
+ {
+ log.fatal( "Invalid collection entry in ReferenceManager.buildKeyLists().", e );
+ }
+ }
+
+
+ /**
+ * Marks the page as referred to by the referrer. If the page does not
+ * exist previously, nothing is done. (This means that some page, somewhere,
+ * has a link to a page that does not exist.)
+ * <P>
+ * This method is NOT synchronized. It should only be referred to from
+ * within a synchronized method, or it should be made synced if necessary.
+ */
+ private void updateReferredBy( String page, String referrer )
+ {
+ // We're not really interested in first level self-references.
+ if( page.equals( referrer ) )
+ {
+ return;
+ }
+
+ // Neither are we interested if plural forms refer to each other.
+ if( m_matchEnglishPlurals )
+ {
+ String p2 = page.endsWith("s") ? page.substring(0,page.length()-1) : page+"s";
+
+ if( referrer.equals(p2) )
+ {
+ return;
+ }
+ }
+
+ Set<String> referrers = m_referredBy.get( page );
+
+ // Even if 'page' has not been created yet, it can still be referenced.
+ // This requires we don't use m_referredBy keys when looking up missing
+ // pages, of course.
+ if(referrers == null)
+ {
+ referrers = new TreeSet<String>();
+ m_referredBy.put( page, referrers );
+ }
+ referrers.add( referrer );
+ }
+
+
+ /**
+ * Clears the references to a certain page so it's no longer in the map.
+ *
+ * @param pagename Name of the page to clear references for.
+ */
+ public synchronized void clearPageEntries( String pagename )
+ {
+ pagename = getFinalPageName(pagename);
+
+ //
+ // Remove this item from the referredBy list of any page
+ // which this item refers to.
+ //
+ Collection<String> c = m_refersTo.get( pagename );
+
+ if( c != null )
+ {
+ for( Iterator<String> i = c.iterator(); i.hasNext(); )
+ {
+ Collection<String> dref = m_referredBy.get( i.next() );
+
+ dref.remove( pagename );
+ }
+ }
+
+ //
+ // Finally, remove direct references.
+ //
+ m_referredBy.remove( pagename );
+ m_refersTo.remove( pagename );
+ }
+
+
+ /**
+ * Finds all unreferenced pages. This requires a linear scan through
+ * m_referredBy to locate keys with null or empty values.
+ */
+ public synchronized Collection<String> findUnreferenced()
+ {
+ List<String> unref = new ArrayList<String>();
+
+ Iterator<String> it = m_referredBy.keySet().iterator();
+
+ while( it.hasNext() )
+ {
+ String key = it.next();
+ //Set refs = (Set) m_referredBy.get( key );
+ Set refs = getReferenceList( m_referredBy, key );
+ if( refs == null || refs.isEmpty() )
+ {
+ unref.add( key );
+ }
+ }
+
+ return unref;
+ }
+
+
+ /**
+ * Finds all references to non-existant pages. This requires a linear
+ * scan through m_refersTo values; each value must have a corresponding
+ * key entry in the reference Maps, otherwise such a page has never
+ * been created.
+ * <P>
+ * Returns a Collection containing Strings of unreferenced page names.
+ * Each non-existant page name is shown only once - we don't return information
+ * on who referred to it.
+ */
+ public synchronized Collection<String> findUncreated()
+ {
+ Set<String> uncreated = new TreeSet<String>();
+
+ // Go through m_refersTo values and check that m_refersTo has the corresponding keys.
+ // We want to reread the code to make sure our HashMaps are in sync...
+
+ for( Collection<String> refs : m_refersTo.values())
+ {
+ if( refs != null )
+ {
+ for ( String aReference : refs )
+ {
+ if( m_engine.pageExists( aReference ) == false )
+ {
+ uncreated.add( aReference );
+ }
+ }
+ }
+ }
+
+ return uncreated;
+ }
+
+ /**
+ * Searches for the given page in the given Map.
+ */
+ private Set<String> getReferenceList( Map<String,Set<String>> coll, String pagename )
+ {
+ Set<String> refs = coll.get( pagename );
+
+ if( m_matchEnglishPlurals )
+ {
+ //
+ // We'll add also matches from the "other" page.
+ //
+ Set<String> refs2;
+
+ if( pagename.endsWith("s") )
+ {
+ refs2 = coll.get( pagename.substring(0,pagename.length()-1) );
+ }
+ else
+ {
+ refs2 = coll.get( pagename+"s" );
+ }
+
+ if( refs2 != null )
+ {
+ if( refs != null )
+ refs.addAll( refs2 );
+ else
+ refs = refs2;
+ }
+ }
+ return refs;
+ }
+
+ /**
+ * Find all pages that refer to this page. Returns null if the page
+ * does not exist or is not referenced at all, otherwise returns a
+ * collection containing page names (String) that refer to this one.
+ * <p>
+ * @param pagename The page to find referrers for.
+ * @return A Collection of Strings. (This is, in fact, a Set, and is likely
+ * to change at some point to a Set). May return null, if the page
+ * does not exist, or if it has no references.
+ */
+ // FIXME: Return a Set instead of a Collection.
+ public synchronized Collection<String> findReferrers( String pagename )
+ {
+ Set<String> refs = getReferenceList( m_referredBy, pagename );
+
+ if( refs == null || refs.isEmpty() )
+ {
+ return null;
+ }
+
+ return refs;
+
+ }
+
+ /**
+ * Returns all pages that refer to this page. Note that this method
+ * returns an unmodifiable Map, which may be abruptly changed. So any
+ * access to any iterator may result in a ConcurrentModificationException.
+ * <p>
+ * The advantages of using this method over findReferrers() is that
+ * it is very fast, as it does not create a new object. The disadvantages
+ * are that it does not do any mapping between plural names, and you
+ * may end up getting a ConcurrentModificationException.
+ *
+ * @param pageName Page name to query.
+ * @return A Set of Strings containing the names of all the pages that refer
+ * to this page. May return null, if the page does not exist or
+ * has not been indexed yet.
+ * @since 2.2.33
+ */
+ public Set findReferredBy( String pageName )
+ {
+ return m_unmutableReferredBy.get( getFinalPageName(pageName) );
+ }
+
+ /**
+ * Returns all pages that this page refers to. You can use this as a quick
+ * way of getting the links from a page, but note that it does not link any
+ * InterWiki, image, or external links. It does contain attachments, though.
+ * <p>
+ * The Collection returned is unmutable, so you cannot change it. It does reflect
+ * the current status and thus is a live object. So, if you are using any
+ * kind of an iterator on it, be prepared for ConcurrentModificationExceptions.
+ * <p>
+ * The returned value is a Collection, because a page may refer to another page
+ * multiple times.
+ *
+ * @param pageName Page name to query
+ * @return A Collection of Strings containing the names of the pages that this page
+ * refers to. May return null, if the page does not exist or has not
+ * been indexed yet.
+ * @since 2.2.33
+ */
+ public Collection<String> findRefersTo( String pageName )
+ {
+ return m_unmutableRefersTo.get( getFinalPageName(pageName) );
+ }
+
+ /**
+ * This 'deepHashCode' can be used to determine if there were any
+ * modifications made to the underlying to and by maps of the
+ * ReferenceManager. The maps of the ReferenceManager are not
+ * synchronized, so someone could add/remove entries in them while the
+ * hashCode is being computed.
+ *
+ * @return Sum of the hashCodes for the to and by maps of the
+ * ReferenceManager
+ * @since 2.3.24
+ */
+ /*
+ This method traps and retries if a concurrent
+ modifcaition occurs.
+ TODO: It is unnecessary to calculate the hashcode; it should be calculated only
+ when the hashmaps are changed. This is slow.
+ */
+ public int deepHashCode()
+ {
+ boolean failed = true;
+ int signature = 0;
+
+ while (failed)
+ {
+ signature = 0;
+ try
+ {
+ signature ^= m_referredBy.hashCode();
+ signature ^= m_refersTo.hashCode();
+ failed = false;
+ }
+ catch (ConcurrentModificationException e)
+ {
+ Thread.yield();
+ }
+ }
+
+ return signature;
+ }
+
+ /**
+ * Returns a list of all pages that the ReferenceManager knows about.
+ * This should be roughly equivalent to PageManager.getAllPages(), but without
+ * the potential disk access overhead. Note that this method is not guaranteed
+ * to return a Set of really all pages (especially during startup), but it is
+ * very fast.
+ *
+ * @return A Set of all defined page names that ReferenceManager knows about.
+ * @since 2.3.24
+ */
+ public Set<String> findCreated()
+ {
+ return new HashSet<String>( m_refersTo.keySet() );
+ }
+
+ private String getFinalPageName( String orig )
+ {
+ try
+ {
+ String s = m_engine.getFinalPageName( orig );
+
+ if( s == null ) s = orig;
+
+ return s;
+ }
+ catch( ProviderException e )
+ {
+ log.error("Error while trying to fetch a page name; trying to cope with the situation.",e);
+
+ return orig;
+ }
+ }
+
+ public void actionPerformed(WikiEvent event)
+ {
+ if( (event instanceof WikiPageEvent) && (event.getType() == WikiPageEvent.PAGE_DELETED) )
+ {
+ String pageName = ((WikiPageEvent) event).getPageName();
+
+ if( pageName != null )
+ {
+ pageRemoved( pageName );
+ }
+ }
+ }
+}
Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/Release.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/Release.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/Release.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/Release.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,193 @@
+/*
+ JSPWiki - a JSP-based WikiWiki clone.
+
+ Copyright (C) 2001-2007 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser 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.ecyrd.jspwiki;
+
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Contains release and version information. You may also invoke this
+ * class directly, in which case it prints out the version string. This
+ * is a handy way of checking which JSPWiki version you have - just type
+ * from a command line:
+ * <pre>
+ * % java -cp JSPWiki.jar com.ecyrd.jspwiki.Release
+ * 2.5.38
+ * </pre>
+ * <p>
+ * As a historical curiosity, this is the oldest JSPWiki file. According
+ * to the CVS history, it dates from 6.7.2001, and it really hasn't changed
+ * much since.
+ * </p>
+ * @author Janne Jalkanen
+ * @since 1.0
+ */
+public final class Release
+{
+ private static final String VERSION_SEPARATORS = ".-";
+
+ /**
+ * This is the default application name.
+ */
+ public static final String APPNAME = "JSPWiki";
+
+ /**
+ * This should be empty when doing a release - otherwise
+ * keep it as "cvs" so that whenever someone checks out the code,
+ * they know it is a bleeding-edge version. Other possible
+ * values are "alpha" and "beta" for alpha and beta versions,
+ * respectively.
+ * <p>
+ * If the POSTFIX is empty, it is not added to the version string.
+ */
+ private static final String POSTFIX = "rc";
+
+ /** The JSPWiki major version. */
+ public static final int VERSION = 2;
+
+ /** The JSPWiki revision. */
+ public static final int REVISION = 6;
+
+ /** The minor revision. */
+ public static final int MINORREVISION = 1;
+
+ /** The build number/identifier. This is a String as opposed to an integer, just
+ * so that people can add other identifiers to it. The build number is incremented
+ * every time a committer checks in code, and reset when the a release is made.
+ * <p>
+ * If you are a person who likes to build his own releases, we recommend that you
+ * add your initials to this identifier (e.g. "13-jj", or 49-aj").
+ * <p>
+ * If the build identifier is empty, it is not added.
+ */
+ public static final String BUILD = "4";
+
+ /**
+ * This is the generic version string you should use
+ * when printing out the version. It is of the form "VERSION.REVISION.MINORREVISION[-POSTFIX][-BUILD]".
+ */
+ public static final String VERSTR =
+ VERSION+"."+REVISION+"."+MINORREVISION+ ((POSTFIX.length() != 0 ) ? "-"+POSTFIX : "") + ((BUILD.length() != 0 ? "-"+BUILD : ""));
+
+ /**
+ * Private constructor prevents instantiation.
+ */
+ private Release()
+ {}
+
+ /**
+ * This method is useful for templates, because hopefully it will
+ * not be inlined, and thus any change to version number does not
+ * need recompiling the pages.
+ *
+ * @since 2.1.26.
+ * @return The version string (e.g. 2.5.23).
+ */
+ public static String getVersionString()
+ {
+ return VERSTR;
+ }
+
+ /**
+ * Returns true, if this version of JSPWiki is newer or equal than what is requested.
+ * @param version A version parameter string (a.b.c-something). B and C are optional.
+ * @return A boolean value describing whether the given version is newer than the current JSPWiki.
+ * @since 2.4.57
+ * @throws IllegalArgumentException If the version string could not be parsed.
+ */
+ public static boolean isNewerOrEqual( String version )
+ throws IllegalArgumentException
+ {
+ if( version == null ) return true;
+ String[] versionComponents = StringUtils.split(version,VERSION_SEPARATORS);
+ int reqVersion = versionComponents.length > 0 ? Integer.parseInt(versionComponents[0]) : Release.VERSION;
+ int reqRevision = versionComponents.length > 1 ? Integer.parseInt(versionComponents[1]) : Release.REVISION;
+ int reqMinorRevision = versionComponents.length > 2 ? Integer.parseInt(versionComponents[2]) : Release.MINORREVISION;
+
+ if( VERSION == reqVersion )
+ {
+ if( REVISION == reqRevision )
+ {
+ if( MINORREVISION == reqMinorRevision )
+ {
+ return true;
+ }
+
+ return MINORREVISION > reqMinorRevision;
+ }
+
+ return REVISION > reqRevision;
+ }
+
+ return VERSION > reqVersion;
+ }
+
+ /**
+ * Returns true, if this version of JSPWiki is older or equal than what is requested.
+ * @param version A version parameter string (a.b.c-something)
+ * @return A boolean value describing whether the given version is older than the current JSPWiki version
+ * @since 2.4.57
+ * @throws IllegalArgumentException If the version string could not be parsed.
+ */
+ public static boolean isOlderOrEqual( String version )
+ throws IllegalArgumentException
+ {
+ if( version == null ) return true;
+
+ String[] versionComponents = StringUtils.split(version,VERSION_SEPARATORS);
+ int reqVersion = versionComponents.length > 0 ? Integer.parseInt(versionComponents[0]) : Release.VERSION;
+ int reqRevision = versionComponents.length > 1 ? Integer.parseInt(versionComponents[1]) : Release.REVISION;
+ int reqMinorRevision = versionComponents.length > 2 ? Integer.parseInt(versionComponents[2]) : Release.MINORREVISION;
+
+ if( VERSION == reqVersion )
+ {
+ if( REVISION == reqRevision )
+ {
+ if( MINORREVISION == reqMinorRevision )
+ {
+ return true;
+ }
+
+ return MINORREVISION < reqMinorRevision;
+ }
+
+ return REVISION < reqRevision;
+ }
+
+ return VERSION < reqVersion;
+ }
+
+ /**
+ * Executing this class directly from command line prints out
+ * the current version. It is very useful for things like
+ * different command line tools.
+ * <P>Example:
+ * <PRE>
+ * % java com.ecyrd.jspwiki.Release
+ * 1.9.26-cvs
+ * </PRE>
+ *
+ * @param argv The argument string. This class takes in no arguments.
+ */
+ public static void main( String[] argv )
+ {
+ System.out.println(VERSTR);
+ }
+}
\ No newline at end of file
Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchMatcher.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchMatcher.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchMatcher.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchMatcher.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,192 @@
+/*
+ JSPWiki - a JSP-based WikiWiki clone.
+
+ Copyright (C) 2001 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser 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.ecyrd.jspwiki;
+
+
+import java.io.IOException;
+import java.io.BufferedReader;
+import java.io.StringReader;
+
+/**
+ * SearchMatcher performs the task of matching a search query to a page's
+ * contents. This utility class is isolated to simplify WikiPageProvider
+ * implementations and to offer an easy target for upgrades. The upcoming(?)
+ * TranslatorReader rewrite will presumably invalidate this, among other things.
+ *
+ * @since 2.1.5
+ * @author ebu at ecyrd dot com
+ */
+// FIXME: Move to the "search" -package in 3.0
+public class SearchMatcher
+{
+ private QueryItem[] m_queries;
+ private WikiEngine m_engine;
+
+ /**
+ * Creates a new SearchMatcher.
+ *
+ * @param engine The WikiEngine
+ * @param queries A list of queries
+ */
+ public SearchMatcher( WikiEngine engine, QueryItem[] queries )
+ {
+ m_engine = engine;
+ m_queries = queries;
+ }
+
+ /**
+ * Compares the page content, available through the given stream, to the
+ * query items of this matcher. Returns a search result object describing
+ * the quality of the match.
+ *
+ * <p>This method would benefit of regexps (1.4) and streaming. FIXME!
+ *
+ * @param wikiname The name of the page
+ * @param pageText The content of the page
+ * @return A SearchResult item, or null, there are no queries
+ * @throws IOException If reading page content fails
+ */
+ public SearchResult matchPageContent( String wikiname, String pageText )
+ throws IOException
+ {
+ if( m_queries == null )
+ {
+ return null;
+ }
+
+ int[] scores = new int[ m_queries.length ];
+ BufferedReader in = new BufferedReader( new StringReader( pageText ) );
+ String line = null;
+
+ while( (line = in.readLine()) != null )
+ {
+ line = line.toLowerCase();
+
+ for( int j = 0; j < m_queries.length; j++ )
+ {
+ int index = -1;
+
+ while( (index = line.indexOf( m_queries[j].word, index+1 )) != -1 )
+ {
+ if( m_queries[j].type != QueryItem.FORBIDDEN )
+ {
+ scores[j]++; // Mark, found this word n times
+ }
+ else
+ {
+ // Found something that was forbidden.
+ return null;
+ }
+ }
+ }
+ }
+
+ //
+ // Check that we have all required words.
+ //
+
+ int totalscore = 0;
+
+ for( int j = 0; j < scores.length; j++ )
+ {
+ // Give five points for each occurrence
+ // of the word in the wiki name.
+
+ if( wikiname.toLowerCase().indexOf( m_queries[j].word ) != -1 &&
+ m_queries[j].type != QueryItem.FORBIDDEN )
+ scores[j] += 5;
+
+ // Filter out pages if the search word is marked 'required'
+ // but they have no score.
+
+ if( m_queries[j].type == QueryItem.REQUIRED && scores[j] == 0 )
+ {
+ return null;
+ }
+
+ //
+ // Count the total score for this page.
+ //
+ totalscore += scores[j];
+ }
+
+ if( totalscore > 0 )
+ {
+ return new SearchResultImpl( wikiname, totalscore );
+ }
+
+ return null;
+ }
+
+ /**
+ * A local search result.
+ *
+ */
+ public class SearchResultImpl
+ implements SearchResult
+ {
+ int m_score;
+ WikiPage m_page;
+
+ /**
+ * Create a new SearchResult with a given name and a score.
+ *
+ * @param name Page Name
+ * @param score A score from 0+
+ */
+ public SearchResultImpl( String name, int score )
+ {
+ m_page = new WikiPage( m_engine, name );
+ m_score = score;
+ }
+
+ /**
+ * Returns Wikipage for this result.
+ * @return WikiPage
+ */
+ public WikiPage getPage()
+ {
+ return m_page;
+ }
+
+ /**
+ * Returns a score for this match.
+ *
+ * @return Score from 0+
+ */
+ public int getScore()
+ {
+ return m_score;
+ }
+
+ /**
+ * Returns an empty array, since BasicSearchProvider does not support
+ * context matching.
+ *
+ * @return an empty array
+ */
+ public String[] getContexts()
+ {
+ // Unimplemented
+ return new String[0];
+ }
+ }
+
+}
Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResult.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResult.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResult.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResult.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,54 @@
+/*
+ JSPWiki - a JSP-based WikiWiki clone.
+
+ Copyright (C) 2001 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser 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.ecyrd.jspwiki;
+
+/**
+ * Defines a search result.
+ *
+ * @author Janne Jalkanen
+ */
+// FIXME3.0: Move to the search-package
+public interface SearchResult
+{
+ /**
+ * Return the page.
+ *
+ * @return the WikiPage object containing this result
+ */
+ public WikiPage getPage();
+
+ /**
+ * Returns the score.
+ *
+ * @return A positive score value. Note that there is no upper limit for the score.
+ */
+
+ public int getScore();
+
+
+ /**
+ * Collection of XHTML fragments representing some contexts in which
+ * the match was made (a.k.a., "snippets").
+ *
+ * @return the search results
+ * @since 2.4
+ */
+ public String[] getContexts();
+}
Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResultComparator.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResultComparator.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResultComparator.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResultComparator.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,52 @@
+/*
+ JSPWiki - a JSP-based WikiWiki clone.
+
+ Copyright (C) 2001 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser 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.ecyrd.jspwiki;
+
+import java.io.Serializable;
+import java.util.*;
+
+/**
+ * Simple class that decides which search results are more
+ * important than others.
+ */
+// FIXME3.0: move to the search package
+public class SearchResultComparator
+ implements Comparator<SearchResult>, Serializable
+{
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Compares two SearchResult objects, returning
+ * the one that scored higher.
+ *
+ * {@inheritDoc}
+ */
+ public int compare( SearchResult s1, SearchResult s2 )
+ {
+ // Bigger scores are first.
+
+ int res = s2.getScore() - s1.getScore();
+
+ if( res == 0 )
+ res = s1.getPage().getName().compareTo(s2.getPage().getName());
+
+ return res;
+ }
+}
Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/StringTransmutator.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/StringTransmutator.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/StringTransmutator.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/StringTransmutator.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,37 @@
+/*
+ JSPWiki - a JSP-based WikiWiki clone.
+
+ Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser 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.ecyrd.jspwiki;
+
+/**
+ * Defines an interface for transforming strings within a Wiki context.
+ *
+ * @since 1.6.4
+ */
+public interface StringTransmutator
+{
+ /**
+ * Returns a changed String, suitable for Wiki context.
+ *
+ * @param context WikiContext in which mutation is to be done
+ * @param source The source string.
+ * @return The mutated string.
+ */
+ public String mutate( WikiContext context, String source );
+}
Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/TextUtil.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/TextUtil.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/TextUtil.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/TextUtil.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,831 @@
+/*
+ JSPWiki - a JSP-based WikiWiki clone.
+
+ Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser 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.ecyrd.jspwiki;
+
+import java.io.UnsupportedEncodingException;
+import java.security.SecureRandom;
+import java.util.Properties;
+import java.util.Random;
+
+
+/**
+ * Contains a number of static utility methods.
+ */
+// FIXME3.0: Move to the "util" package
+public final class TextUtil
+{
+ static final String HEX_DIGITS = "0123456789ABCDEF";
+
+ /**
+ * Private constructor prevents instantiation.
+ */
+ private TextUtil()
+ {}
+
+ /**
+ * java.net.URLEncoder.encode() method in JDK < 1.4 is buggy. This duplicates
+ * its functionality.
+ * @param rs the string to encode
+ * @return the URL-encoded string
+ */
+ protected static String urlEncode( byte[] rs )
+ {
+ StringBuffer result = new StringBuffer(rs.length*2);
+
+ // Does the URLEncoding. We could use the java.net one, but
+ // it does not eat byte[]s.
+
+ for( int i = 0; i < rs.length; i++ )
+ {
+ char c = (char) rs[i];
+
+ switch( c )
+ {
+ case '_':
+ case '.':
+ case '*':
+ case '-':
+ case '/':
+ result.append( c );
+ break;
+
+ case ' ':
+ result.append( '+' );
+ break;
+
+ default:
+ if( (c >= 'a' && c <= 'z') ||
+ (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') )
+ {
+ result.append( c );
+ }
+ else
+ {
+ result.append( '%' );
+ result.append( HEX_DIGITS.charAt( (c & 0xF0) >> 4 ) );
+ result.append( HEX_DIGITS.charAt( c & 0x0F ) );
+ }
+ }
+
+ } // for
+
+ return result.toString();
+ }
+
+ /**
+ * URL encoder does not handle all characters correctly.
+ * See <A HREF="http://developer.java.sun.com/developer/bugParade/bugs/4257115.html">
+ * Bug parade, bug #4257115</A> for more information.
+ * <P>
+ * Thanks to CJB for this fix.
+ *
+ * @param bytes The byte array containing the bytes of the string
+ * @param encoding The encoding in which the string should be interpreted
+ * @return A decoded String
+ *
+ * @throws UnsupportedEncodingException If the encoding is unknown.
+ * @throws IllegalArgumentException If the byte array is not a valid string.
+ */
+ protected static String urlDecode( byte[] bytes, String encoding )
+ throws UnsupportedEncodingException,
+ IllegalArgumentException
+ {
+ if(bytes == null)
+ {
+ return null;
+ }
+
+ byte[] decodeBytes = new byte[bytes.length];
+ int decodedByteCount = 0;
+
+ try
+ {
+ for( int count = 0; count < bytes.length; count++ )
+ {
+ switch( bytes[count] )
+ {
+ case '+':
+ decodeBytes[decodedByteCount++] = (byte) ' ';
+ break ;
+
+ case '%':
+ decodeBytes[decodedByteCount++] = (byte)((HEX_DIGITS.indexOf(bytes[++count]) << 4) +
+ (HEX_DIGITS.indexOf(bytes[++count])) );
+
+ break ;
+
+ default:
+ decodeBytes[decodedByteCount++] = bytes[count] ;
+ }
+ }
+
+ }
+ catch (IndexOutOfBoundsException ae)
+ {
+ throw new IllegalArgumentException( "Malformed UTF-8 string?" );
+ }
+
+ String processedPageName = null ;
+
+ try
+ {
+ processedPageName = new String(decodeBytes, 0, decodedByteCount, encoding) ;
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ throw new UnsupportedEncodingException( "UTF-8 encoding not supported on this platform" );
+ }
+
+ return processedPageName;
+ }
+
+ /**
+ * As java.net.URLEncoder class, but this does it in UTF8 character set.
+ *
+ * @param text The text to decode
+ * @return An URLEncoded string.
+ */
+ public static String urlEncodeUTF8( String text )
+ {
+ // If text is null, just return an empty string
+ if ( text == null )
+ {
+ return "";
+ }
+
+ byte[] rs;
+
+ try
+ {
+ rs = text.getBytes("UTF-8");
+ return urlEncode( rs );
+ }
+ catch( UnsupportedEncodingException e )
+ {
+ throw new InternalWikiException("UTF-8 not supported!?!");
+ }
+
+ }
+
+ /**
+ * As java.net.URLDecoder class, but for UTF-8 strings. null is a safe
+ * value and returns null.
+ *
+ * @param utf8 The UTF-8 encoded string
+ * @return A plain, normal string.
+ */
+ public static String urlDecodeUTF8( String utf8 )
+ {
+ String rs = null;
+
+ if( utf8 == null ) return null;
+
+ try
+ {
+ rs = urlDecode( utf8.getBytes("ISO-8859-1"), "UTF-8" );
+ }
+ catch( UnsupportedEncodingException e )
+ {
+ throw new InternalWikiException("UTF-8 or ISO-8859-1 not supported!?!");
+ }
+
+ return rs;
+ }
+
+ /**
+ * Provides encoded version of string depending on encoding.
+ * Encoding may be UTF-8 or ISO-8859-1 (default).
+ *
+ * <p>This implementation is the same as in
+ * FileSystemProvider.mangleName().
+ *
+ * @param data A string to encode
+ * @param encoding The encoding in which to encode
+ * @return An URL encoded string.
+ */
+ public static String urlEncode( String data, String encoding )
+ {
+ // Presumably, the same caveats apply as in FileSystemProvider.
+ // Don't see why it would be horribly kludgy, though.
+ if( "UTF-8".equals( encoding ) )
+ {
+ return TextUtil.urlEncodeUTF8( data );
+ }
+
+ try
+ {
+ return TextUtil.urlEncode( data.getBytes(encoding) );
+ }
+ catch (UnsupportedEncodingException uee)
+ {
+ throw new InternalWikiException("Could not encode String into" + encoding);
+ }
+ }
+
+ /**
+ * Provides decoded version of string depending on encoding.
+ * Encoding may be UTF-8 or ISO-8859-1 (default).
+ *
+ * <p>This implementation is the same as in
+ * FileSystemProvider.unmangleName().
+ *
+ * @param data The URL-encoded string to decode
+ * @param encoding The encoding to use
+ * @return A decoded string.
+ * @throws UnsupportedEncodingException If the encoding is unknown
+ * @throws IllegalArgumentException If the data cannot be decoded.
+ */
+ public static String urlDecode( String data, String encoding )
+ throws UnsupportedEncodingException,
+ IllegalArgumentException
+ {
+ // Presumably, the same caveats apply as in FileSystemProvider.
+ // Don't see why it would be horribly kludgy, though.
+ if( "UTF-8".equals( encoding ) )
+ {
+ return TextUtil.urlDecodeUTF8( data );
+ }
+
+ try
+ {
+ return TextUtil.urlDecode( data.getBytes(encoding), encoding );
+ }
+ catch (UnsupportedEncodingException uee)
+ {
+ throw new InternalWikiException("Could not decode String into" + encoding);
+ }
+
+ }
+
+ /**
+ * Replaces the relevant entities inside the String.
+ * All & >, <, and " are replaced by their
+ * respective names.
+ *
+ * @since 1.6.1
+ * @param src The source string.
+ * @return The encoded string.
+ */
+ public static String replaceEntities( String src )
+ {
+ src = replaceString( src, "&", "&" );
+ src = replaceString( src, "<", "<" );
+ src = replaceString( src, ">", ">" );
+ src = replaceString( src, "\"", """ );
+
+ return src;
+ }
+
+ /**
+ * Replaces a string with an other string.
+ *
+ * @param orig Original string. Null is safe.
+ * @param src The string to find.
+ * @param dest The string to replace <I>src</I> with.
+ * @return A string with the replacement done.
+ */
+ public static final String replaceString( String orig, String src, String dest )
+ {
+ if ( orig == null ) return null;
+ if ( src == null || dest == null ) throw new NullPointerException();
+ if ( src.length() == 0 ) return orig;
+
+ StringBuffer res = new StringBuffer(orig.length()+20); // Pure guesswork
+ int start = 0;
+ int end = 0;
+ int last = 0;
+
+ while ( (start = orig.indexOf(src,end)) != -1 )
+ {
+ res.append( orig.substring( last, start ) );
+ res.append( dest );
+ end = start+src.length();
+ last = start+src.length();
+ }
+
+ res.append( orig.substring( end ) );
+
+ return res.toString();
+ }
+
+ /**
+ * Replaces a part of a string with a new String.
+ *
+ * @param start Where in the original string the replacing should start.
+ * @param end Where the replacing should end.
+ * @param orig Original string. Null is safe.
+ * @param text The new text to insert into the string.
+ * @return The string with the orig replaced with text.
+ */
+ public static String replaceString( String orig, int start, int end, String text )
+ {
+ if( orig == null ) return null;
+
+ StringBuffer buf = new StringBuffer(orig);
+
+ buf.replace( start, end, text );
+
+ return buf.toString();
+ }
+
+ /**
+ * Parses an integer parameter, returning a default value
+ * if the value is null or a non-number.
+ *
+ * @param value The value to parse
+ * @param defvalue A default value in case the value is not a number
+ * @return The parsed value (or defvalue).
+ */
+
+ public static int parseIntParameter( String value, int defvalue )
+ {
+ int val = defvalue;
+
+ try
+ {
+ val = Integer.parseInt( value.trim() );
+ }
+ catch( Exception e ) {}
+
+ return val;
+ }
+
+ /**
+ * Gets an integer-valued property from a standard Properties
+ * list. If the value does not exist, or is a non-integer, returns defVal.
+ *
+ * @since 2.1.48.
+ * @param props The property set to look through
+ * @param key The key to look for
+ * @param defVal If the property is not found or is a non-integer, returns this value.
+ * @return The property value as an integer (or defVal).
+ */
+ public static int getIntegerProperty( Properties props,
+ String key,
+ int defVal )
+ {
+ String val = props.getProperty( key );
+
+ return parseIntParameter( val, defVal );
+ }
+
+ /**
+ * Gets a boolean property from a standard Properties list.
+ * Returns the default value, in case the key has not been set.
+ * <P>
+ * The possible values for the property are "true"/"false", "yes"/"no", or
+ * "on"/"off". Any value not recognized is always defined as "false".
+ *
+ * @param props A list of properties to search.
+ * @param key The property key.
+ * @param defval The default value to return.
+ *
+ * @return True, if the property "key" was set to "true", "on", or "yes".
+ *
+ * @since 2.0.11
+ */
+ public static boolean getBooleanProperty( Properties props,
+ String key,
+ boolean defval )
+ {
+ String val = props.getProperty( key );
+
+ if( val == null ) return defval;
+
+ return isPositive( val );
+ }
+
+ /**
+ * Fetches a String property from the set of Properties. This differs from
+ * Properties.getProperty() in a couple of key respects: First, property value
+ * is trim()med (so no extra whitespace back and front), and well, that's it.
+ *
+ * @param props The Properties to search through
+ * @param key The property key
+ * @param defval A default value to return, if the property does not exist.
+ * @return The property value.
+ * @since 2.1.151
+ */
+ public static String getStringProperty( Properties props,
+ String key,
+ String defval )
+ {
+ String val = props.getProperty( key );
+
+ if( val == null ) return defval;
+
+ return val.trim();
+ }
+
+ /**
+ * Returns true, if the string "val" denotes a positive string. Allowed
+ * values are "yes", "on", and "true". Comparison is case-insignificant.
+ * Null values are safe.
+ *
+ * @param val Value to check.
+ * @return True, if val is "true", "on", or "yes"; otherwise false.
+ *
+ * @since 2.0.26
+ */
+ public static boolean isPositive( String val )
+ {
+ if( val == null ) return false;
+
+ val = val.trim();
+
+ return val.equalsIgnoreCase("true") || val.equalsIgnoreCase("on") ||
+ val.equalsIgnoreCase("yes");
+ }
+
+ /**
+ * Makes sure that the POSTed data is conforms to certain rules. These
+ * rules are:
+ * <UL>
+ * <LI>The data always ends with a newline (some browsers, such
+ * as NS4.x series, does not send a newline at the end, which makes
+ * the diffs a bit strange sometimes.
+ * <LI>The CR/LF/CRLF mess is normalized to plain CRLF.
+ * </UL>
+ *
+ * The reason why we're using CRLF is that most browser already
+ * return CRLF since that is the closest thing to a HTTP standard.
+ *
+ * @param postData The data to normalize
+ * @return Normalized data
+ */
+ public static String normalizePostData( String postData )
+ {
+ StringBuffer sb = new StringBuffer();
+
+ for( int i = 0; i < postData.length(); i++ )
+ {
+ switch( postData.charAt(i) )
+ {
+ case 0x0a: // LF, UNIX
+ sb.append( "\r\n" );
+ break;
+
+ case 0x0d: // CR, either Mac or MSDOS
+ sb.append( "\r\n" );
+ // If it's MSDOS, skip the LF so that we don't add it again.
+ if( i < postData.length()-1 && postData.charAt(i+1) == 0x0a )
+ {
+ i++;
+ }
+ break;
+
+ default:
+ sb.append( postData.charAt(i) );
+ break;
+ }
+ }
+
+ if( sb.length() < 2 || !sb.substring( sb.length()-2 ).equals("\r\n") )
+ {
+ sb.append( "\r\n" );
+ }
+
+ return sb.toString();
+ }
+
+ private static final int EOI = 0;
+ private static final int LOWER = 1;
+ private static final int UPPER = 2;
+ private static final int DIGIT = 3;
+ private static final int OTHER = 4;
+ private static final Random RANDOM = new SecureRandom();
+
+ private static int getCharKind(int c)
+ {
+ if (c==-1)
+ {
+ return EOI;
+ }
+
+ char ch = (char) c;
+
+ if (Character.isLowerCase(ch))
+ return LOWER;
+ else if (Character.isUpperCase(ch))
+ return UPPER;
+ else if (Character.isDigit(ch))
+ return DIGIT;
+ else
+ return OTHER;
+ }
+
+ /**
+ * Adds spaces in suitable locations of the input string. This is
+ * used to transform a WikiName into a more readable format.
+ *
+ * @param s String to be beautified.
+ * @return A beautified string.
+ */
+ public static String beautifyString( String s )
+ {
+ return beautifyString( s, " " );
+ }
+
+ /**
+ * Adds spaces in suitable locations of the input string. This is
+ * used to transform a WikiName into a more readable format.
+ *
+ * @param s String to be beautified.
+ * @param space Use this string for the space character.
+ * @return A beautified string.
+ * @since 2.1.127
+ */
+ public static String beautifyString( String s, String space )
+ {
+ StringBuffer result = new StringBuffer();
+
+ if( s == null || s.length() == 0 ) return "";
+
+ int cur = s.charAt(0);
+ int curKind = getCharKind(cur);
+
+ int prevKind = LOWER;
+ int nextKind = -1;
+
+ int next = -1;
+ int nextPos = 1;
+
+ while( curKind != EOI )
+ {
+ next = (nextPos < s.length()) ? s.charAt(nextPos++) : -1;
+ nextKind = getCharKind( next );
+
+ if( (prevKind == UPPER) && (curKind == UPPER) && (nextKind == LOWER) )
+ {
+ result.append(space);
+ result.append((char) cur);
+ }
+ else
+ {
+ result.append((char) cur);
+ if( ( (curKind == UPPER) && (nextKind == DIGIT) )
+ || ( (curKind == LOWER) && ((nextKind == DIGIT) || (nextKind == UPPER)) )
+ || ( (curKind == DIGIT) && ((nextKind == UPPER) || (nextKind == LOWER)) ))
+ {
+ result.append(space);
+ }
+ }
+ prevKind = curKind;
+ cur = next;
+ curKind = nextKind;
+ }
+
+ return result.toString();
+ }
+
+ /**
+ * Creates a Properties object based on an array which contains alternatively
+ * a key and a value. It is useful for generating default mappings.
+ * For example:
+ * <pre>
+ * String[] properties = { "jspwiki.property1", "value1",
+ * "jspwiki.property2", "value2 };
+ *
+ * Properties props = TextUtil.createPropertes( values );
+ *
+ * System.out.println( props.getProperty("jspwiki.property1") );
+ * </pre>
+ * would output "value1".
+ *
+ * @param values Alternating key and value pairs.
+ * @return Property object
+ * @see java.util.Properties
+ * @throws IllegalArgumentException if the property array is missing
+ * a value for a key.
+ * @since 2.2.
+ */
+
+ public static Properties createProperties( String[] values )
+ throws IllegalArgumentException
+ {
+ if( values.length % 2 != 0 )
+ throw new IllegalArgumentException( "One value is missing.");
+
+ Properties props = new Properties();
+
+ for( int i = 0; i < values.length; i += 2 )
+ {
+ props.setProperty( values[i], values[i+1] );
+ }
+
+ return props;
+ }
+
+ /**
+ * Counts the number of sections (separated with "----") from the page.
+ *
+ * @param pagedata The WikiText to parse.
+ * @return int Number of counted sections.
+ * @since 2.1.86.
+ */
+
+ public static int countSections( String pagedata )
+ {
+ int tags = 0;
+ int start = 0;
+
+ while( (start = pagedata.indexOf("----",start)) != -1 )
+ {
+ tags++;
+ start+=4; // Skip this "----"
+ }
+
+ //
+ // The first section does not get the "----"
+ //
+ return pagedata.length() > 0 ? tags+1 : 0;
+ }
+
+ /**
+ * Gets the given section (separated with "----") from the page text.
+ * Note that the first section is always #1. If a page has no section markers,
+ * them there is only a single section, #1.
+ *
+ * @param pagedata WikiText to parse.
+ * @param section Which section to get.
+ * @return String The section.
+ * @throws IllegalArgumentException If the page does not contain this many sections.
+ * @since 2.1.86.
+ */
+ public static String getSection( String pagedata, int section )
+ throws IllegalArgumentException
+ {
+ int tags = 0;
+ int start = 0;
+ int previous = 0;
+
+ while( (start = pagedata.indexOf("----",start)) != -1 )
+ {
+ if( ++tags == section )
+ {
+ return pagedata.substring( previous, start );
+ }
+
+ start += 4; // Skip this "----"
+
+ previous = start;
+ }
+
+ if( ++tags == section )
+ {
+ return pagedata.substring( previous );
+ }
+
+ throw new IllegalArgumentException("There is no section no. "+section+" on the page.");
+ }
+
+ /**
+ * A simple routine which just repeates the arguments. This is useful
+ * for creating something like a line or something.
+ *
+ * @param what String to repeat
+ * @param times How many times to repeat the string.
+ * @return Guess what?
+ * @since 2.1.98.
+ */
+ public static String repeatString( String what, int times )
+ {
+ StringBuffer sb = new StringBuffer();
+
+ for( int i = 0; i < times; i++ )
+ {
+ sb.append( what );
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Converts a string from the Unicode representation into something that can be
+ * embedded in a java properties file. All references outside the ASCII range
+ * are replaced with \\uXXXX.
+ *
+ * @param s The string to convert
+ * @return the ASCII string
+ */
+ public static String native2Ascii(String s)
+ {
+ StringBuffer sb = new StringBuffer();
+ for(int i = 0; i < s.length(); i++)
+ {
+ char aChar = s.charAt(i);
+ if ((aChar < 0x0020) || (aChar > 0x007e))
+ {
+ sb.append('\\');
+ sb.append('u');
+ sb.append(toHex((aChar >> 12) & 0xF));
+ sb.append(toHex((aChar >> 8) & 0xF));
+ sb.append(toHex((aChar >> 4) & 0xF));
+ sb.append(toHex( aChar & 0xF));
+ }
+ else
+ {
+ sb.append(aChar);
+ }
+ }
+ return sb.toString();
+ }
+
+ private static char toHex(int nibble)
+ {
+ final char[] hexDigit =
+ {
+ '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'
+ };
+ return hexDigit[nibble & 0xF];
+ }
+
+ /**
+ * Generates a hexadecimal string from an array of bytes. For
+ * example, if the array contains { 0x01, 0x02, 0x3E }, the resulting
+ * string will be "01023E".
+ *
+ * @param bytes A Byte array
+ * @return A String representation
+ * @since 2.3.87
+ */
+ public static String toHexString( byte[] bytes )
+ {
+ StringBuffer sb = new StringBuffer( bytes.length*2 );
+ for( int i = 0; i < bytes.length; i++ )
+ {
+ sb.append( toHex(bytes[i] >> 4) );
+ sb.append( toHex(bytes[i]) );
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns true, if the argument contains a number, otherwise false.
+ * In a quick test this is roughly the same speed as Integer.parseInt()
+ * if the argument is a number, and roughly ten times the speed, if
+ * the argument is NOT a number.
+ *
+ * @since 2.4
+ * @param s String to check
+ * @return True, if s represents a number. False otherwise.
+ */
+
+ public static boolean isNumber( String s )
+ {
+ if( s == null ) return false;
+
+ if( s.length() > 1 && s.charAt(0) == '-' )
+ s = s.substring(1);
+
+ for( int i = 0; i < s.length(); i++ )
+ {
+ if( !Character.isDigit(s.charAt(i)) )
+ return false;
+ }
+
+ return true;
+ }
+
+ /** Length of password. @see #generateRandomPassword() */
+ public static final int PASSWORD_LENGTH = 8;
+ /**
+ * Generate a random String suitable for use as a temporary password.
+ *
+ * @return String suitable for use as a temporary password
+ * @since 2.4
+ */
+ public static String generateRandomPassword()
+ {
+ // Pick from some letters that won't be easily mistaken for each
+ // other. So, for example, omit o O and 0, 1 l and L.
+ String letters = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789+@";
+
+ String pw = "";
+ for (int i=0; i<PASSWORD_LENGTH; i++)
+ {
+ int index = (int)(RANDOM.nextDouble()*letters.length());
+ pw += letters.substring(index, index+1);
+ }
+ return pw;
+ }
+}