KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > dspace > browse > Browse


1 /*
2  * Browse.java
3  *
4  * Version: $Revision: 1.47 $
5  *
6  * Date: $Date: 2006/09/12 11:22:13 $
7  *
8  * Copyright (c) 2002-2005, Hewlett-Packard Company and Massachusetts
9  * Institute of Technology. All rights reserved.
10  *
11  * Redistribution and use in source and binary forms, with or without
12  * modification, are permitted provided that the following conditions are
13  * met:
14  *
15  * - Redistributions of source code must retain the above copyright
16  * notice, this list of conditions and the following disclaimer.
17  *
18  * - Redistributions in binary form must reproduce the above copyright
19  * notice, this list of conditions and the following disclaimer in the
20  * documentation and/or other materials provided with the distribution.
21  *
22  * - Neither the name of the Hewlett-Packard Company nor the name of the
23  * Massachusetts Institute of Technology nor the names of their
24  * contributors may be used to endorse or promote products derived from
25  * this software without specific prior written permission.
26  *
27  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
30  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
31  * HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
32  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
33  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
34  * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
35  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
36  * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
37  * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
38  * DAMAGE.
39  */

40 package org.dspace.browse;
41
42 import java.sql.Connection JavaDoc;
43 import java.sql.PreparedStatement JavaDoc;
44 import java.sql.ResultSet JavaDoc;
45 import java.sql.SQLException JavaDoc;
46 import java.text.MessageFormat JavaDoc;
47 import java.util.ArrayList JavaDoc;
48 import java.util.Collections JavaDoc;
49 import java.util.HashMap JavaDoc;
50 import java.util.Iterator JavaDoc;
51 import java.util.List JavaDoc;
52 import java.util.Map JavaDoc;
53 import java.util.SortedMap JavaDoc;
54 import java.util.StringTokenizer JavaDoc;
55 import java.util.TreeMap JavaDoc;
56 import java.util.WeakHashMap JavaDoc;
57
58 import org.apache.log4j.Logger;
59 import org.dspace.content.Collection;
60 import org.dspace.content.Community;
61 import org.dspace.content.DCValue;
62 import org.dspace.content.Item;
63 import org.dspace.content.ItemComparator;
64 import org.dspace.content.ItemIterator;
65 import org.dspace.core.ConfigurationManager;
66 import org.dspace.core.Context;
67 import org.dspace.storage.rdbms.DatabaseManager;
68 import org.dspace.storage.rdbms.TableRow;
69
70 /**
71  * API for Browsing Items in DSpace by title, author, or date. Browses only
72  * return archived Items.
73  *
74  * @author Peter Breton
75  * @version $Revision: 1.47 $
76  */

77 public class Browse
78 {
79     // Browse types
80
static final int AUTHORS_BROWSE = 0;
81
82     static final int ITEMS_BY_TITLE_BROWSE = 1;
83
84     static final int ITEMS_BY_AUTHOR_BROWSE = 2;
85
86     static final int ITEMS_BY_DATE_BROWSE = 3;
87
88     static final int SUBJECTS_BROWSE = 4;
89
90     static final int ITEMS_BY_SUBJECT_BROWSE = 5;
91
92     
93     /** Log4j log */
94     private static Logger log = Logger.getLogger(Browse.class);
95
96     /**
97      * Constructor
98      */

99     private Browse()
100     {
101     }
102
103     /**
104      * Return distinct Authors in the given scope. Author refers to a Dublin
105      * Core field with element <em>contributor</em> and qualifier
106      * <em>author</em>.
107      *
108      * <p>
109      * Results are returned in alphabetical order.
110      * </p>
111      *
112      * @param scope
113      * The BrowseScope
114      * @return A BrowseInfo object, the results of the browse
115      * @exception SQLException
116      * If a database error occurs
117      */

118     public static BrowseInfo getAuthors(BrowseScope scope) throws SQLException JavaDoc
119     {
120         scope.setBrowseType(AUTHORS_BROWSE);
121         scope.setAscending(true);
122         scope.setSortByTitle(null);
123
124         return doBrowse(scope);
125     }
126     /**
127      * Return distinct Subjects in the given scope. Subjects refers to a Dublin
128      * Core field with element <em>subject</em> and qualifier
129      * <em>*</em>.
130      *
131      * <p>
132      * Results are returned in alphabetical order.
133      * </p>
134      *
135      * @param scope
136      * The BrowseScope
137      * @return A BrowseInfo object, the results of the browse
138      * @exception SQLException
139      * If a database error occurs
140      */

141     public static BrowseInfo getSubjects(BrowseScope scope) throws SQLException JavaDoc
142     {
143         scope.setBrowseType(SUBJECTS_BROWSE);
144         scope.setAscending(true);
145         scope.setSortByTitle(null);
146
147         return doBrowse(scope);
148     }
149
150     /**
151      * Return Items indexed by title in the given scope. Title refers to a
152      * Dublin Core field with element <em>title</em> and no qualifier.
153      *
154      * <p>
155      * Results are returned in alphabetical order; that is, the Item with the
156      * title which is first in alphabetical order will be returned first.
157      * </p>
158      *
159      * @param scope
160      * The BrowseScope
161      * @return A BrowseInfo object, the results of the browse
162      * @exception SQLException
163      * If a database error occurs
164      */

165     public static BrowseInfo getItemsByTitle(BrowseScope scope)
166             throws SQLException JavaDoc
167     {
168         scope.setBrowseType(ITEMS_BY_TITLE_BROWSE);
169         scope.setAscending(true);
170         scope.setSortByTitle(null);
171
172         return doBrowse(scope);
173     }
174
175     /**
176      * Return Items indexed by date in the given scope. Date refers to a Dublin
177      * Core field with element <em>date</em> and qualifier <em>issued</em>.
178      *
179      * <p>
180      * If oldestfirst is true, the dates returned are the ones after the focus,
181      * ordered from earliest to latest. Otherwise the dates are the ones before
182      * the focus, and ordered from latest to earliest. For example:
183      * </p>
184      *
185      * <p>
186      * For example, if the focus is <em>1995</em>, and oldestfirst is true,
187      * the results might look like this:
188      * </p>
189      *
190      * <code>1993, 1994, 1995 (the focus), 1996, 1997.....</code>
191      *
192      * <p>
193      * While if the focus is <em>1995</em>, and oldestfirst is false, the
194      * results would be:
195      * </p>
196      *
197      * <code>1997, 1996, 1995 (the focus), 1994, 1993 .....</code>
198      *
199      * @param scope
200      * The BrowseScope
201      * @param oldestfirst
202      * If true, the dates returned are the ones after focus, ordered
203      * from earliest to latest; otherwise the dates are the ones
204      * before focus, ordered from latest to earliest.
205      * @return A BrowseInfo object, the results of the browse
206      * @exception SQLException
207      * If a database error occurs
208      */

209     public static BrowseInfo getItemsByDate(BrowseScope scope,
210             boolean oldestfirst) throws SQLException JavaDoc
211     {
212         scope.setBrowseType(ITEMS_BY_DATE_BROWSE);
213         scope.setAscending(oldestfirst);
214         scope.setSortByTitle(null);
215
216         return doBrowse(scope);
217     }
218
219     /**
220      * <p>
221      * Return Items in the given scope by the author (exact match). The focus of
222      * the BrowseScope is the author to use; using a BrowseScope without a focus
223      * causes an IllegalArgumentException to be thrown.
224      * </p>
225      *
226      * <p>
227      * Author refers to a Dublin Core field with element <em>contributor</em>
228      * and qualifier <em>author</em>.
229      * </p>
230      *
231      * @param scope
232      * The BrowseScope
233      * @param sortByTitle
234      * If true, the returned items are sorted by title; otherwise
235      * they are sorted by date issued.
236      * @return A BrowseInfo object, the results of the browse
237      * @exception SQLException
238      * If a database error occurs
239      */

240     public static BrowseInfo getItemsByAuthor(BrowseScope scope,
241             boolean sortByTitle) throws SQLException JavaDoc
242     {
243         if (!scope.hasFocus())
244         {
245             throw new IllegalArgumentException JavaDoc(
246                     "Must specify an author for getItemsByAuthor");
247         }
248
249         if (!(scope.getFocus() instanceof String JavaDoc))
250         {
251             throw new IllegalArgumentException JavaDoc(
252                     "The focus for getItemsByAuthor must be a String");
253         }
254
255         scope.setBrowseType(ITEMS_BY_AUTHOR_BROWSE);
256         scope.setAscending(true);
257         scope.setSortByTitle(sortByTitle ? Boolean.TRUE : Boolean.FALSE);
258         scope.setTotalAll();
259
260         return doBrowse(scope);
261     }
262
263     /**
264      * <p>
265      * Return Items in the given scope by the subject (exact match). The focus of
266      * the BrowseScope is the subject to use; using a BrowseScope without a focus
267      * causes an IllegalArgumentException to be thrown.
268      * </p>
269      *
270      * <p>
271      * Subject refers to a Dublin Core field with element <em>subject</em>
272      * and qualifier <em>*</em>.
273      * </p>
274      *
275      * @param scope
276      * The BrowseScope
277      * @param sortByTitle
278      * If true, the returned items are sorted by title; otherwise
279      * they are sorted by date issued.
280      * @return A BrowseInfo object, the results of the browse
281      * @exception SQLException
282      * If a database error occurs
283      */

284     public static BrowseInfo getItemsBySubject(BrowseScope scope,
285             boolean sortByTitle) throws SQLException JavaDoc
286     {
287         if (!scope.hasFocus())
288         {
289             throw new IllegalArgumentException JavaDoc(
290                     "Must specify a subject for getItemsBySubject");
291         }
292
293         if (!(scope.getFocus() instanceof String JavaDoc))
294         {
295             throw new IllegalArgumentException JavaDoc(
296                     "The focus for getItemsBySubject must be a String");
297         }
298
299         scope.setBrowseType(ITEMS_BY_SUBJECT_BROWSE);
300         scope.setAscending(true);
301         scope.setSortByTitle(sortByTitle ? Boolean.TRUE : Boolean.FALSE);
302         scope.setTotalAll();
303
304         return doBrowse(scope);
305     }
306
307     /**
308      * Returns the last items submitted to DSpace in the given scope.
309      *
310      * @param scope
311      * The Browse Scope
312      * @return A List of Items submitted
313      * @exception SQLException
314      * If a database error occurs
315      */

316     public static List JavaDoc getLastSubmitted(BrowseScope scope) throws SQLException JavaDoc
317     {
318         Context context = scope.getContext();
319         String JavaDoc sql = getLastSubmittedQuery(scope);
320
321         if (log.isDebugEnabled())
322         {
323             log.debug("SQL for last submitted is \"" + sql + "\"");
324         }
325
326         List JavaDoc results = DatabaseManager.query(context, sql).toList();
327
328         return getLastSubmittedResults(context, results);
329     }
330
331     /**
332      * Return the SQL used to determine the last submitted Items for scope.
333      *
334      * @param scope
335      * @return String query string
336      */

337     private static String JavaDoc getLastSubmittedQuery(BrowseScope scope)
338     {
339         String JavaDoc table = getLastSubmittedTable(scope);
340
341         String JavaDoc query = "SELECT * FROM " + table
342                 + getScopeClause(scope, "where")
343                 + " ORDER BY date_accessioned DESC";
344
345         if (!scope.hasNoLimit())
346         {
347             if ("oracle".equals(ConfigurationManager.getProperty("db.name")))
348             {
349                 // Oracle version of LIMIT...OFFSET - must use a sub-query and
350
// ROWNUM
351
query = "SELECT * FROM (" + query + ") WHERE ROWNUM <="
352                         + scope.getTotal();
353             }
354             else
355             {
356                 // postgres, use LIMIT
357
query = query + " LIMIT " + scope.getTotal();
358             }
359         }
360
361         return query;
362     }
363
364     /**
365      * Return the name of the Browse index table to query for last submitted
366      * items in the given scope.
367      *
368      * @param scope
369      * @return name of table
370      */

371     private static String JavaDoc getLastSubmittedTable(BrowseScope scope)
372     {
373         if (scope.isCommunityScope())
374         {
375             return "CommunityItemsByDateAccession";
376         }
377         else if (scope.isCollectionScope())
378         {
379             return "CollectionItemsByDateAccession";
380         }
381
382         return "ItemsByDateAccessioned";
383     }
384
385     /**
386      * Transform the query results into a List of Items.
387      *
388      * @param context
389      * @param results
390      * @return list of items
391      * @throws SQLException
392      */

393     private static List JavaDoc getLastSubmittedResults(Context context, List JavaDoc results)
394             throws SQLException JavaDoc
395     {
396         if ((results == null) || (results.isEmpty()))
397         {
398             return Collections.EMPTY_LIST;
399         }
400
401         List JavaDoc items = new ArrayList JavaDoc();
402
403         // FIXME This seems like a very common need, so might
404
// be factored out at some point.
405
for (Iterator JavaDoc iterator = results.iterator(); iterator.hasNext();)
406         {
407             TableRow row = (TableRow) iterator.next();
408             Item item = Item.find(context, row.getIntColumn("item_id"));
409             items.add(item);
410         }
411
412         return items;
413     }
414
415     ////////////////////////////////////////
416
// Index maintainence methods
417
////////////////////////////////////////
418

419     /**
420      * This method should be called whenever an item is removed.
421      *
422      * @param context
423      * The current DSpace context
424      * @param id
425      * The id of the item which has been removed
426      * @exception SQLException
427      * If a database error occurs
428      */

429     public static void itemRemoved(Context context, int id) throws SQLException JavaDoc
430     {
431         String JavaDoc sql = "delete from {0} where item_id = " + id;
432
433         String JavaDoc[] browseTables = BrowseTables.tables();
434
435         for (int i = 0; i < browseTables.length; i++)
436         {
437             String JavaDoc query = MessageFormat.format(sql,
438                     new String JavaDoc[] { browseTables[i] });
439             DatabaseManager.updateQuery(context, query);
440         }
441     }
442
443     /**
444      * This method should be called whenever an item has changed. Changes
445      * include:
446      *
447      * <ul>
448      * <li>DC values are added, removed, or modified
449      * <li>the value of the in_archive flag changes
450      * </ul>
451      *
452      * @param context -
453      * The database context
454      * @param item -
455      * The item which has been added
456      * @exception SQLException -
457      * If a database error occurs
458      */

459     public static void itemChanged(Context context, Item item)
460             throws SQLException JavaDoc
461     {
462         // This is a bit heavy-weight, but without knowing what
463
// changed, it's easiest just to replace the values
464
// en masse.
465
itemRemoved(context, item.getID());
466
467         if (!item.isArchived())
468         {
469             return;
470         }
471
472         itemAdded(context, item);
473     }
474
475     /**
476      * This method should be called whenever an item is added.
477      *
478      * @param context
479      * The current DSpace context
480      * @param item
481      * The item which has been added
482      * @exception SQLException
483      * If a database error occurs
484      */

485     public static void itemAdded(Context context, Item item)
486             throws SQLException JavaDoc
487     {
488         // add all parent communities to communities2item table
489
Community[] parents = item.getCommunities();
490
491         for (int j = 0; j < parents.length; j++)
492         {
493             TableRow row = DatabaseManager.create(context, "Communities2Item");
494             row.setColumn("item_id", item.getID());
495             row.setColumn("community_id", parents[j].getID());
496             DatabaseManager.update(context, row);
497         }
498
499         // get the metadata fields to index in the title and date tables
500

501         // get the date, title and author fields
502
String JavaDoc dateField = ConfigurationManager.getProperty("webui.browse.index.date");
503         if (dateField == null)
504         {
505             dateField = "dc.date.issued";
506         }
507         
508         String JavaDoc titleField = ConfigurationManager.getProperty("webui.browse.index.title");
509         if (titleField == null)
510         {
511             titleField = "dc.title";
512         }
513         
514         String JavaDoc authorField = ConfigurationManager.getProperty("webui.browse.index.author");
515         if (authorField == null)
516         {
517             authorField = "dc.contributor.*";
518         }
519         
520         String JavaDoc subjectField = ConfigurationManager.getProperty("webui.browse.index.subject");
521         if (subjectField == null)
522         {
523             subjectField = "dc.subject.*";
524         }
525         
526         // get the DC values for each of these fields
527
DCValue[] titleArray = getMetadataField(item, titleField);
528         DCValue[] dateArray = getMetadataField(item, dateField);
529         DCValue[] authorArray = getMetadataField(item, authorField);
530         DCValue[] subjectArray = getMetadataField(item, subjectField);
531         
532         // now build the data map
533
Map JavaDoc table2dc = new HashMap JavaDoc();
534         table2dc.put("ItemsByTitle", titleArray);
535         table2dc.put("ItemsByAuthor", authorArray);
536         table2dc.put("ItemsByDate", dateArray);
537         table2dc.put("ItemsByDateAccessioned", item.getDC("date",
538                 "accessioned", Item.ANY));
539         table2dc.put("ItemsBySubject", subjectArray);
540
541         for (Iterator JavaDoc iterator = table2dc.keySet().iterator(); iterator
542                 .hasNext();)
543         {
544             String JavaDoc table = (String JavaDoc) iterator.next();
545             DCValue[] dc = (DCValue[]) table2dc.get(table);
546
547             for (int i = 0; i < dc.length; i++)
548             {
549                 TableRow row = DatabaseManager.create(context, table);
550                 row.setColumn("item_id", item.getID());
551
552                 String JavaDoc value = dc[i].value;
553
554                 if ("ItemsByDateAccessioned".equals(table))
555                 {
556                     row.setColumn("date_accessioned", value);
557                 }
558                 else if ("ItemsByDate".equals(table))
559                 {
560                     row.setColumn("date_issued", value);
561                 }
562                 else if ("ItemsByAuthor".equals(table))
563                 {
564                     // author name, and normalized sorting name
565
// (which for now is simple lower-case)
566
row.setColumn("author", value);
567                     row.setColumn("sort_author", value.toLowerCase());
568                 }
569                 else if ("ItemsByTitle".equals(table))
570                 {
571                     String JavaDoc title = NormalizedTitle.normalize(value,
572                             dc[i].language);
573                     row.setColumn("title", value);
574                     row.setColumn("sort_title", title.toLowerCase());
575                 }
576                 else if ("ItemsBySubject".equals(table))
577                 {
578                     row.setColumn("subject", value);
579                     row.setColumn("sort_subject", value.toLowerCase());
580                 }
581
582
583                 DatabaseManager.update(context, row);
584             }
585         }
586     }
587
588     /**
589      * Index all items in DSpace. This method may be resource-intensive.
590      *
591      * @param context
592      * Current DSpace context
593      * @return The number of items indexed.
594      * @exception SQLException
595      * If a database error occurs
596      */

597     public static int indexAll(Context context) throws SQLException JavaDoc
598     {
599         indexRemoveAll(context);
600
601         int count = 0;
602         ItemIterator iterator = Item.findAll(context);
603
604         while (iterator.hasNext())
605         {
606             itemAdded(context, iterator.next());
607             count++;
608         }
609
610         return count;
611     }
612
613     /**
614      * Remove all items in DSpace from the Browse index.
615      *
616      * @param context
617      * Current DSpace context
618      * @return The number of items removed.
619      * @exception SQLException
620      * If a database error occurs
621      */

622     public static int indexRemoveAll(Context context) throws SQLException JavaDoc
623     {
624         int total = 0;
625
626         String JavaDoc[] browseTables = BrowseTables.tables();
627
628         for (int i = 0; i < browseTables.length; i++)
629         {
630             String JavaDoc sql = "delete from " + browseTables[i];
631             total += DatabaseManager.updateQuery(context, sql);
632         }
633
634         return total;
635     }
636
637     ////////////////////////////////////////
638
// Other methods
639
////////////////////////////////////////
640

641     /**
642      * Return the normalized form of title.
643      *
644      * @param title
645      * @param lang
646      * @return title
647      */

648     public static String JavaDoc getNormalizedTitle(String JavaDoc title, String JavaDoc lang)
649     {
650         return NormalizedTitle.normalize(title, lang);
651     }
652
653     ////////////////////////////////////////
654
// Private methods
655
////////////////////////////////////////
656

657     private static DCValue[] getMetadataField(Item item, String JavaDoc md)
658     {
659         StringTokenizer JavaDoc dcf = new StringTokenizer JavaDoc(md, ".");
660         
661         String JavaDoc[] tokens = { "", "", "" };
662         int i = 0;
663         while(dcf.hasMoreTokens())
664         {
665             tokens[i] = dcf.nextToken().toLowerCase().trim();
666             i++;
667         }
668         String JavaDoc schema = tokens[0];
669         String JavaDoc element = tokens[1];
670         String JavaDoc qualifier = tokens[2];
671         
672         DCValue[] values;
673         if ("*".equals(qualifier))
674         {
675             values = item.getMetadata(schema, element, Item.ANY, Item.ANY);
676         }
677         else if ("".equals(qualifier))
678         {
679             values = item.getMetadata(schema, element, null, Item.ANY);
680         }
681         else
682         {
683             values = item.getMetadata(schema, element, qualifier, Item.ANY);
684         }
685         
686         return values;
687     }
688     
689     /**
690      * Workhorse method for browse functionality.
691      *
692      * @param scope
693      * @return BrowseInfo
694      * @throws SQLException
695      */

696     private static BrowseInfo doBrowse(BrowseScope scope) throws SQLException JavaDoc
697     {
698         // Check for a cached browse
699
BrowseInfo cachedInfo = BrowseCache.get(scope);
700
701         if (cachedInfo != null)
702         {
703             return cachedInfo;
704         }
705
706         // Run the Browse queries
707
// If the focus is an Item, this returns the value
708
String JavaDoc itemValue = getItemValue(scope);
709         List JavaDoc results = new ArrayList JavaDoc();
710         results.addAll(getResultsBeforeFocus(scope, itemValue));
711
712         int beforeFocus = results.size();
713         results.addAll(getResultsAfterFocus(scope, itemValue, beforeFocus));
714
715         // Find out the total in the index, and the number of
716
// matches for the query
717
int total = countTotalInIndex(scope, results.size());
718         int matches = countMatches(scope, itemValue, total, results.size());
719
720         if (log.isDebugEnabled())
721             {
722             log.debug("Number of matches " + matches);
723         }
724
725         int position = getPosition(total, matches, beforeFocus);
726
727         sortResults(scope, results);
728
729             BrowseInfo info = new BrowseInfo(results, position, total,
730                     beforeFocus);
731
732         logInfo(info);
733
734         BrowseCache.add(scope, info);
735
736         return info;
737     }
738
739     /**
740      * If focus refers to an Item, return a value for the item (its title,
741      * author, accession date, etc). Otherwise return null.
742      *
743      * In general, the queries for these values look like this: select
744      * max(date_issued) from ItemsByDate where item_id = 7;
745      *
746      * The max operator ensures that only one value is returned.
747      *
748      * If limiting to a community or collection, we add a clause like:
749      * community_id = 7 collection_id = 201
750      *
751      * @param scope
752      * @return desired value for the item
753      * @throws SQLException
754      */

755     protected static String JavaDoc getItemValue(BrowseScope scope) throws SQLException JavaDoc
756     {
757         if (!scope.focusIsItem())
758         {
759             return null;
760         }
761
762         PreparedStatement JavaDoc statement = null;
763         ResultSet JavaDoc results = null;
764
765         try
766         {
767             String JavaDoc tablename = BrowseTables.getTable(scope);
768             String JavaDoc column = BrowseTables.getValueColumn(scope);
769
770             String JavaDoc itemValueQuery = new StringBuffer JavaDoc().append("select ")
771                     .append("max(").append(column).append(") from ").append(
772                             tablename).append(" where ").append(" item_id = ")
773                     .append(scope.getFocusItemId()).append(
774                             getScopeClause(scope, "and")).toString();
775
776             statement = createStatement(scope, itemValueQuery);
777             results = statement.executeQuery();
778
779             String JavaDoc itemValue = results.next() ? results.getString(1) : null;
780
781             if (log.isDebugEnabled())
782             {
783                 log.debug("Subquery value is " + itemValue);
784             }
785
786             return itemValue;
787         }
788         finally
789         {
790             if (statement != null)
791             {
792                 statement.close();
793             }
794
795             if (results != null)
796             {
797                 results.close();
798             }
799         }
800     }
801
802     /**
803      * Run a database query and return results before the focus.
804      *
805      * @param scope
806      * The Browse Scope
807      * @param itemValue
808      * If the focus is an Item, this is its value in the index (its
809      * title, author, etc).
810      * @return list of Item results
811      * @throws SQLException
812      */

813     protected static List JavaDoc getResultsBeforeFocus(BrowseScope scope,
814             String JavaDoc itemValue) throws SQLException JavaDoc
815     {
816         // Starting from beginning of index
817
if (!scope.hasFocus())
818         {
819             return Collections.EMPTY_LIST;
820         }
821
822         // No previous results desired
823
if (scope.getNumberBefore() == 0)
824         {
825             return Collections.EMPTY_LIST;
826         }
827
828         // ItemsByAuthor. Since this is an exact match, it
829
// does not make sense to return values before the
830
// query.
831
if (scope.getBrowseType() == ITEMS_BY_AUTHOR_BROWSE
832                 || scope.getBrowseType() == ITEMS_BY_SUBJECT_BROWSE)
833         {
834             return Collections.EMPTY_LIST;
835         }
836
837         PreparedStatement JavaDoc statement = createSql(scope, itemValue, false, false);
838
839         List JavaDoc qresults = DatabaseManager.queryPrepared(statement).toList();
840         int numberDesired = scope.getNumberBefore();
841         List JavaDoc results = getResults(scope, qresults, numberDesired);
842
843         if (!results.isEmpty())
844         {
845             Collections.reverse(results);
846         }
847
848         return results;
849     }
850
851     /**
852      * Run a database query and return results after the focus.
853      *
854      * @param scope
855      * The Browse Scope
856      * @param itemValue
857      * If the focus is an Item, this is its value in the index (its
858      * title, author, etc).
859      * @param count
860      * @return list of results after the focus
861      * @throws SQLException
862      */

863     protected static List JavaDoc getResultsAfterFocus(BrowseScope scope,
864             String JavaDoc itemValue, int count) throws SQLException JavaDoc
865     {
866         // No results desired
867
if (scope.getTotal() == 0)
868         {
869             return Collections.EMPTY_LIST;
870         }
871
872         PreparedStatement JavaDoc statement = createSql(scope, itemValue, true, false);
873
874         List JavaDoc qresults = DatabaseManager.queryPrepared(statement).toList();
875
876         // The number of results we want is either -1 (everything)
877
// or the total, less the number already retrieved.
878
int numberDesired = -1;
879
880         if (!scope.hasNoLimit())
881         {
882             numberDesired = Math.max(scope.getTotal() - count, 0);
883         }
884
885         return getResults(scope, qresults, numberDesired);
886     }
887
888     /*
889      * Return the total number of values in an index.
890      *
891      * <p> We total Authors with SQL like: select count(distinct author) from
892      * ItemsByAuthor; ItemsByAuthor with: select count(*) from ItemsByAuthor
893      * where author = ?; and every other index with: select count(*) from
894      * ItemsByTitle; </p>
895      *
896      * <p> If limiting to a community or collection, we add a clause like:
897      * community_id = 7 collection_id = 201 </p>
898      */

899     protected static int countTotalInIndex(BrowseScope scope,
900             int numberOfResults) throws SQLException JavaDoc
901     {
902         int browseType = scope.getBrowseType();
903
904         // When finding Items by Author, it often happens that
905
// we find every single Item (eg, the Author only published
906
// 2 works, and we asked for 15), and so can skip the
907
// query.
908
if ((browseType == ITEMS_BY_AUTHOR_BROWSE)
909                 && (scope.hasNoLimit() || (scope.getTotal() > numberOfResults)))
910         {
911             return numberOfResults;
912         }
913
914         PreparedStatement JavaDoc statement = null;
915         Object JavaDoc obj = scope.getScope();
916
917         try
918         {
919             String JavaDoc table = BrowseTables.getTable(scope);
920
921             StringBuffer JavaDoc buffer = new StringBuffer JavaDoc().append("select count(")
922                     .append(getTargetColumns(scope)).append(") from ").append(
923                             table);
924
925             boolean hasWhere = false;
926
927             if (browseType == ITEMS_BY_AUTHOR_BROWSE)
928             {
929                 hasWhere = true;
930                 buffer.append(" where sort_author = ?");
931             }
932
933             if (browseType == ITEMS_BY_SUBJECT_BROWSE)
934             {
935                 hasWhere = true;
936                 buffer.append(" where sort_subject = ?");
937             }
938
939             String JavaDoc connector = hasWhere ? "and" : "where";
940             String JavaDoc sql = buffer.append(getScopeClause(scope, connector))
941                     .toString();
942
943             if (log.isDebugEnabled())
944             {
945                 log.debug("Total sql: \"" + sql + "\"");
946             }
947
948             statement = createStatement(scope, sql);
949
950             if (browseType == ITEMS_BY_AUTHOR_BROWSE)
951             {
952                 statement.setString(1, (String JavaDoc) scope.getFocus());
953             }
954
955             if (browseType == ITEMS_BY_SUBJECT_BROWSE)
956             {
957                 statement.setString(1, (String JavaDoc) scope.getFocus());
958             }
959
960             return getIntValue(statement);
961         }
962         finally
963         {
964             if (statement != null)
965             {
966                 statement.close();
967             }
968         }
969     }
970
971     /**
972      * Return the number of matches for the browse scope.
973      *
974      * @param scope
975      * @param itemValue
976      * item value we're looking for
977      * @param totalInIndex
978      * FIXME ??
979      * @param numberOfResults
980      * FIXME ??
981      * @return number of matches
982      * @throws SQLException
983      */

984     protected static int countMatches(BrowseScope scope, String JavaDoc itemValue,
985             int totalInIndex, int numberOfResults) throws SQLException JavaDoc
986     {
987         // Matched everything
988
if (numberOfResults == totalInIndex)
989         {
990             return totalInIndex;
991         }
992
993         // Scope matches everything in the index
994
// Note that this only works when the scope is all of DSpace,
995
// since the Community and Collection index tables
996
// include Items in other Communities/Collections
997
if ((!scope.hasFocus()) && scope.isAllDSpaceScope())
998         {
999             return totalInIndex;
1000        }
1001
1002        PreparedStatement JavaDoc statement = null;
1003        try {
1004            statement = createSql(scope, itemValue, true, true);
1005            return getIntValue(statement);
1006        }
1007        finally {
1008            if(statement != null) {
1009                try {
1010                    statement.close();
1011                }
1012                catch(SQLException JavaDoc e) {
1013                    log.error("Problem releasing statement", e);
1014                }
1015            }
1016        }
1017    }
1018
1019    private static int getPosition(int total, int matches, int beforeFocus)
1020    {
1021        // Matched everything, so position is at start (0)
1022
if (total == matches)
1023        {
1024            return 0;
1025        }
1026
1027        return total - matches - beforeFocus;
1028    }
1029
1030    /**
1031     * Sort the results returned from the browse if necessary. The list of
1032     * results is sorted in-place.
1033     *
1034     * @param scope
1035     * @param results
1036     */

1037    private static void sortResults(BrowseScope scope, List JavaDoc results)
1038    {
1039        // Currently we only sort ItemsByAuthor, Advisor, Subjects browses
1040
if ((scope.getBrowseType() != ITEMS_BY_AUTHOR_BROWSE)
1041                && (scope.getBrowseType() != ITEMS_BY_SUBJECT_BROWSE))
1042        {
1043            return;
1044        }
1045
1046        ItemComparator ic = scope.getSortByTitle().booleanValue() ? new ItemComparator(
1047                "title", null, Item.ANY, true)
1048                : new ItemComparator("date", "issued", Item.ANY, true);
1049
1050        Collections.sort(results, ic);
1051    }
1052
1053    /**
1054     * Transform the results of the query (TableRow objects_ into a List of
1055     * Strings (for getAuthors()) or Items (for all the other browses).
1056     *
1057     * @param scope
1058     * The Browse Scope
1059     * @param results
1060     * The results of the query
1061     * @param max
1062     * The maximum number of results to return
1063     * @return FIXME ??
1064     * @throws SQLException
1065     */

1066    private static List JavaDoc getResults(BrowseScope scope, List JavaDoc results, int max)
1067            throws SQLException JavaDoc
1068    {
1069        if (results == null)
1070        {
1071            return Collections.EMPTY_LIST;
1072        }
1073
1074        List JavaDoc theResults = new ArrayList JavaDoc();
1075        boolean hasLimit = !scope.hasNoLimit();
1076        boolean isAuthorsBrowse = scope.getBrowseType() == AUTHORS_BROWSE;
1077        boolean isSubjectsBrowse = scope.getBrowseType() == SUBJECTS_BROWSE;
1078
1079        for (Iterator JavaDoc iterator = results.iterator(); iterator.hasNext();)
1080        {
1081            TableRow row = (TableRow) iterator.next();
1082            Object JavaDoc theValue = null;
1083
1084            if (isAuthorsBrowse)
1085                theValue = (Object JavaDoc) row.getStringColumn("author");
1086            else if (isSubjectsBrowse)
1087                theValue = (Object JavaDoc) row.getStringColumn("subject");
1088            else
1089                theValue = (Object JavaDoc) new Integer JavaDoc(row.getIntColumn("item_id"));
1090
1091            // Should not happen
1092
if (theValue == null)
1093            {
1094                continue;
1095            }
1096
1097            // Exceeded limit
1098
if (hasLimit && (theResults.size() >= max))
1099            {
1100                break;
1101            }
1102
1103            theResults.add(theValue);
1104
1105            if (log.isDebugEnabled())
1106            {
1107                log.debug("Adding result " + theValue);
1108            }
1109        }
1110
1111        return (isAuthorsBrowse||isSubjectsBrowse)
1112                 ? theResults : toItems(scope.getContext(), theResults);
1113    }
1114
1115    /**
1116     * Create a PreparedStatement to run the correct query for scope.
1117     *
1118     * @param scope
1119     * The Browse scope
1120     * @param subqueryValue
1121     * If the focus is an item, this is its value in the browse index
1122     * (its title, author, date, etc). Otherwise null.
1123     * @param after
1124     * If true, create SQL to find the items after the focus.
1125     * Otherwise create SQL to find the items before the focus.
1126     * @param isCount
1127     * If true, create SQL to count the number of matches for the
1128     * query. Otherwise just the query.
1129     * @return a prepared statement
1130     * @throws SQLException
1131     */

1132    private static PreparedStatement JavaDoc createSql(BrowseScope scope,
1133            String JavaDoc subqueryValue, boolean after, boolean isCount)
1134            throws SQLException JavaDoc
1135    {
1136        String JavaDoc sqli = createSqlInternal(scope, subqueryValue, isCount);
1137        String JavaDoc sql = formatSql(scope, sqli, subqueryValue, after);
1138        PreparedStatement JavaDoc statement = createStatement(scope, sql);
1139
1140        // Browses without a focus have no parameters to bind
1141
if (scope.hasFocus())
1142        {
1143            String JavaDoc value = subqueryValue;
1144            if (value == null && scope.getFocus() instanceof String JavaDoc)
1145            {
1146                value = (String JavaDoc)scope.getFocus();
1147            }
1148
1149            statement.setString(1, value);
1150
1151            // Binds the parameter in the subquery clause
1152
if (subqueryValue != null)
1153            {
1154                statement.setString(2, value);
1155            }
1156        }
1157
1158        if (log.isDebugEnabled())
1159        {
1160            log.debug("Created SQL \"" + sql + "\"");
1161        }
1162
1163        return statement;
1164    }
1165
1166    /**
1167     * Create a SQL string to run the correct query.
1168     *
1169     * @param scope
1170     * @param itemValue
1171     * FIXME ??
1172     * @param isCount
1173     * @return
1174     */

1175    private static String JavaDoc createSqlInternal(BrowseScope scope,
1176            String JavaDoc itemValue, boolean isCount)
1177    {
1178        String JavaDoc tablename = BrowseTables.getTable(scope);
1179        String JavaDoc column = BrowseTables.getValueColumn(scope);
1180
1181        int browseType = scope.getBrowseType();
1182
1183        StringBuffer JavaDoc sqlb = new StringBuffer JavaDoc();
1184        sqlb.append("select ");
1185        sqlb.append(isCount ? "count(" : "");
1186        sqlb.append(getTargetColumns(scope));
1187
1188        /**
1189         * This next bit adds another column to the query, so authors don't show
1190         * up lower-case
1191         */

1192        if ((browseType == AUTHORS_BROWSE) && !isCount)
1193        {
1194            sqlb.append(",sort_author");
1195        }
1196
1197        if ((browseType == SUBJECTS_BROWSE) && !isCount)
1198        {
1199            sqlb.append(",sort_subject");
1200        }
1201
1202        sqlb.append(isCount ? ")" : "");
1203
1204        sqlb.append(" from (SELECT DISTINCT * ");
1205
1206        sqlb.append(" from ");
1207        sqlb.append(tablename);
1208        sqlb.append(" ) distinct_view");
1209
1210        // If the browse uses items (or item ids) instead of String values
1211
// make a subquery.
1212
// We use a separate query to make sure the subquery works correctly
1213
// when item values are the same. (this is transactionally
1214
// safe because we set the isolation level).
1215
// If we're NOT searching from the start, add some clauses
1216
boolean addedWhereClause = false;
1217
1218        if (scope.hasFocus())
1219        {
1220            String JavaDoc subquery = null;
1221
1222            if (scope.focusIsItem())
1223            {
1224                subquery = new StringBuffer JavaDoc().append(" or ( ").append(column)
1225                        .append(" = ? and item_id {0} ").append(
1226                                scope.getFocusItemId()).append(")").toString();
1227            }
1228
1229            if (log.isDebugEnabled())
1230            {
1231                log.debug("Subquery is \"" + subquery + "\"");
1232            }
1233
1234            sqlb.append(" where ").append("(").append(column).append(" {1} ")
1235                    .append("?").append(scope.focusIsItem() ? subquery : "")
1236                    .append(")");
1237
1238            addedWhereClause = true;
1239        }
1240
1241        String JavaDoc connector = addedWhereClause ? " and " : " where ";
1242        sqlb.append(getScopeClause(scope, connector));
1243
1244        // For counting, skip the "order by" and "limit" clauses
1245
if (isCount)
1246        {
1247            return sqlb.toString();
1248        }
1249
1250        // Add an order by clause -- a parameter
1251
sqlb
1252                .append(" order by ")
1253                .append(column)
1254                .append("{2}")
1255                .append(
1256                        ((scope.focusIsString() && (scope.getBrowseType() != ITEMS_BY_DATE_BROWSE))
1257                                || (scope.getBrowseType() == AUTHORS_BROWSE) || (scope
1258                                .getBrowseType() == SUBJECTS_BROWSE)) ? ""
1259                                : ", item_id{2}");
1260
1261        String JavaDoc myquery = sqlb.toString();
1262
1263        // A limit on the total returned (Postgres extension)
1264
if (!scope.hasNoLimit())
1265        {
1266            if ("oracle".equals(ConfigurationManager.getProperty("db.name")))
1267            {
1268                myquery = "SELECT * FROM (" + myquery
1269                        + ") WHERE ROWNUM <= {3} ";
1270            }
1271            else
1272            {
1273                // postgres uses LIMIT
1274
myquery = myquery + " LIMIT {3} ";
1275            }
1276        }
1277
1278        return myquery;
1279    }
1280
1281    /**
1282     * Format SQL according to the browse type.
1283     *
1284     * @param scope
1285     * @param sql
1286     * @param subqueryValue
1287     * FIXME ??
1288     * @param after
1289     * @return
1290     *
1291     *
1292     */

1293    private static String JavaDoc formatSql(BrowseScope scope, String JavaDoc sql,
1294            String JavaDoc subqueryValue, boolean after)
1295    {
1296        boolean before = !after;
1297        int browseType = scope.getBrowseType();
1298        boolean ascending = scope.getAscending();
1299        int numberDesired = before ? scope.getNumberBefore() : scope.getTotal();
1300
1301        // Search operator
1302
// Normal case: before is less than, after is greater than or equal
1303
String JavaDoc beforeOperator = "<";
1304        String JavaDoc afterOperator = ">=";
1305
1306        // For authors, only equality is relevant
1307
if (browseType == ITEMS_BY_AUTHOR_BROWSE)
1308        {
1309            afterOperator = "=";
1310        }
1311
1312        if (browseType == ITEMS_BY_SUBJECT_BROWSE)
1313        {
1314            afterOperator = "=";
1315        }
1316
1317        // Subqueries add a clause which checks for the item specifically,
1318
// so we do not check for equality here
1319
if (subqueryValue != null)
1320        {
1321            beforeOperator = "<";
1322            afterOperator = ">";
1323        }
1324
1325        if (!ascending)
1326        {
1327            beforeOperator = ">";
1328            afterOperator = "<=";
1329        }
1330
1331        if (browseType == ITEMS_BY_DATE_BROWSE)
1332        {
1333            if (!ascending)
1334            {
1335                beforeOperator = ">";
1336                afterOperator = "<";
1337            }
1338            else
1339            {
1340                beforeOperator = "<";
1341                afterOperator = ">";
1342            }
1343        }
1344
1345        String JavaDoc beforeSubqueryOperator = "<";
1346        String JavaDoc afterSubqueryOperator = ">=";
1347
1348        // For authors, only equality is relevant
1349
if (browseType == ITEMS_BY_AUTHOR_BROWSE
1350                || browseType == ITEMS_BY_SUBJECT_BROWSE)
1351        {
1352            afterSubqueryOperator = "=";
1353        }
1354
1355        if (!ascending)
1356        {
1357            beforeSubqueryOperator = ">";
1358            afterSubqueryOperator = "<=";
1359        }
1360
1361        String JavaDoc order = before ? " desc" : "";
1362
1363        if (!ascending)
1364        {
1365            order = before ? "" : " desc";
1366        }
1367
1368        // Note that it's OK to have unused arguments in the array;
1369
// see the javadoc of java.text.MessageFormat
1370
// for the whole story.
1371
List JavaDoc args = new ArrayList JavaDoc();
1372
1373        args.add(before ? beforeSubqueryOperator : afterSubqueryOperator);
1374        args.add(before ? beforeOperator : afterOperator);
1375        args.add(order);
1376        args.add(new Integer JavaDoc(numberDesired));
1377
1378        return MessageFormat.format(sql, args.toArray());
1379    }
1380
1381    /**
1382     * Log a message about the results of a browse.
1383     *
1384     * @param info
1385     */

1386    private static void logInfo(BrowseInfo info)
1387    {
1388        if (!log.isDebugEnabled())
1389        {
1390            return;
1391        }
1392
1393        log.debug("Number of Results: " + info.getResultCount()
1394                + " Overall position: " + info.getOverallPosition() + " Total "
1395                + info.getTotal() + " Offset " + info.getOffset());
1396
1397        int lastIndex = (info.getOverallPosition() + info.getResultCount());
1398        boolean noresults = (info.getTotal() == 0)
1399                || (info.getResultCount() == 0);
1400
1401        if (noresults)
1402        {
1403            log.debug("Got no results");
1404        }
1405
1406        log.debug("Got results: " + info.getOverallPosition() + " to "
1407                + lastIndex + " out of " + info.getTotal());
1408    }
1409
1410    /**
1411     * Return the name or names of the column(s) to query for a browse.
1412     *
1413     * @param scope
1414     * The current browse scope
1415     * @return The name or names of the columns to query
1416     */

1417    private static String JavaDoc getTargetColumns(BrowseScope scope)
1418    {
1419        int browseType = scope.getBrowseType();
1420
1421        if (browseType == AUTHORS_BROWSE)
1422            return "distinct author";
1423        else if (browseType == SUBJECTS_BROWSE)
1424            return "distinct subject";
1425        else
1426            return "*";
1427    }
1428
1429    /**
1430     * <p>
1431     * Return a scoping clause.
1432     * </p>
1433     *
1434     * <p>
1435     * If scope is ALLDSPACE_SCOPE, return the empty string.
1436     * </p>
1437     *
1438     * <p>
1439     * Otherwise, the SQL clause which is generated looks like:
1440     * </p>
1441     * CONNECTOR community_id = 7 CONNECTOR collection_id = 203
1442     *
1443     * <p>
1444     * CONNECTOR may be empty, or it may be a SQL keyword like <em>where</em>,
1445     * <em>and</em>, and so forth.
1446     * </p>
1447     *
1448     * @param scope
1449     * @param connector
1450     * FIXME ??
1451     * @return
1452     */

1453    static String JavaDoc getScopeClause(BrowseScope scope, String JavaDoc connector)
1454    {
1455        if (scope.isAllDSpaceScope())
1456        {
1457            return "";
1458        }
1459
1460        boolean isCommunity = scope.isCommunityScope();
1461        Object JavaDoc obj = scope.getScope();
1462
1463        int id = (isCommunity) ? ((Community) obj).getID() : ((Collection) obj)
1464                .getID();
1465
1466        String JavaDoc column = (isCommunity) ? "community_id" : "collection_id";
1467
1468        return new StringBuffer JavaDoc().append(" ").append(connector).append(" ")
1469                .append(column).append(" = ").append(id).toString();
1470    }
1471
1472    /**
1473     * Create a PreparedStatement with the given sql.
1474     *
1475     * @param scope
1476     * The current Browse scope
1477     * @param sql
1478     * SQL query
1479     * @return A PreparedStatement with the given SQL
1480     * @exception SQLException
1481     * If a database error occurs
1482     */

1483    private static PreparedStatement JavaDoc createStatement(BrowseScope scope,
1484            String JavaDoc sql) throws SQLException JavaDoc
1485    {
1486        Connection JavaDoc connection = scope.getContext().getDBConnection();
1487
1488        return connection.prepareStatement(sql);
1489    }
1490
1491    /**
1492     * Return a single int value from the PreparedStatement.
1493     *
1494     * @param statement
1495     * A PreparedStatement for a query which returns a single value
1496     * of INTEGER type.
1497     * @return The integer value from the query.
1498     * @exception SQLException
1499     * If a database error occurs
1500     */

1501    private static int getIntValue(PreparedStatement JavaDoc statement)
1502            throws SQLException JavaDoc
1503    {
1504        ResultSet JavaDoc results = null;
1505
1506        try
1507        {
1508            results = statement.executeQuery();
1509
1510            return results.next() ? results.getInt(1) : (-1);
1511        }
1512        finally
1513        {
1514            if (results != null)
1515            {
1516                results.close();
1517            }
1518        }
1519    }
1520
1521    /**
1522     * Convert a list of item ids to full Items.
1523     *
1524     * @param context
1525     * The current DSpace context
1526     * @param ids
1527     * A list of item ids. Each member of the list is an Integer.
1528     * @return A list of Items with the given ids.
1529     * @exception SQLException
1530     * If a database error occurs
1531     */

1532    private static List JavaDoc toItems(Context context, List JavaDoc ids) throws SQLException JavaDoc
1533    {
1534        // FIXME Again, this is probably a more general need
1535
List JavaDoc results = new ArrayList JavaDoc();
1536
1537        for (Iterator JavaDoc iterator = ids.iterator(); iterator.hasNext();)
1538        {
1539            Integer JavaDoc id = (Integer JavaDoc) iterator.next();
1540            Item item = Item.find(context, id.intValue());
1541
1542            if (item != null)
1543            {
1544                results.add(item);
1545            }
1546        }
1547
1548        return results;
1549    }
1550}
1551
1552class NormalizedTitle
1553{
1554    private static String JavaDoc[] STOP_WORDS = new String JavaDoc[] { "A", "An", "The" };
1555
1556    /**
1557     * Returns a normalized String corresponding to TITLE.
1558     *
1559     * @param title
1560     * @param lang
1561     * @return
1562     */

1563    public static String JavaDoc normalize(String JavaDoc title, String JavaDoc lang)
1564    {
1565        if (lang == null)
1566        {
1567            return title;
1568        }
1569
1570        return (lang.startsWith("en")) ? normalizeEnglish(title) : title;
1571    }
1572
1573    /**
1574     * Returns a normalized String corresponding to TITLE. The normalization is
1575     * effected by:
1576     * + first removing leading spaces, if any + then removing the first
1577     * leading occurences of "a", "an" and "the" (in any case). + removing any
1578     * whitespace following an occurence of a stop word
1579     *
1580     * This simple strategy is only expected to be used for English words.
1581     *
1582     * @param oldtitle
1583     * @return
1584     */

1585    public static String JavaDoc normalizeEnglish(String JavaDoc oldtitle)
1586    {
1587        // Corner cases
1588
if (oldtitle == null)
1589        {
1590            return null;
1591        }
1592
1593        if (oldtitle.length() == 0)
1594        {
1595            return oldtitle;
1596        }
1597
1598        // lower case, stupid! (sorry, just a rant about bad contractors)
1599
String JavaDoc title = oldtitle.toLowerCase();
1600
1601        // State variables
1602
// First find leading whitespace, if any
1603
int startAt = firstWhitespace(title);
1604        boolean modified = (startAt != 0);
1605        boolean usedStopWord = false;
1606        String JavaDoc stop = null;
1607
1608        // Examine each stop word
1609
for (int i = 0; i < STOP_WORDS.length; i++)
1610        {
1611            stop = STOP_WORDS[i];
1612
1613            int stoplen = stop.length();
1614
1615            // The title must start with the stop word (skipping white space
1616
// and ignoring case).
1617
boolean found = title.toLowerCase().startsWith(stop.toLowerCase(),
1618                    startAt)
1619                    && ( // The title must be longer than whitespace plus the
1620
// stop word
1621
title.length() >= (startAt + stoplen + 1)) &&
1622                    // The stop word must be followed by white space
1623
Character.isWhitespace(title.charAt(startAt + stoplen));
1624
1625            if (found)
1626            {
1627                modified = true;
1628                usedStopWord = true;
1629
1630                startAt += stoplen;
1631
1632                // Strip leading whitespace again, if any
1633
int firstw = firstWhitespace(title, startAt);
1634
1635                if (firstw != 0)
1636                {
1637                    startAt = firstw;
1638                }
1639
1640                // Only process a single stop word
1641
break;
1642            }
1643        }
1644
1645        // If we didn't change anything, just return the title as-is
1646
if (!modified)
1647        {
1648            return title;
1649        }
1650
1651        // If we just stripped white space, return a substring
1652
if (!usedStopWord)
1653        {
1654            return title.substring(startAt);
1655        }
1656
1657        // Otherwise, return the substring with the stop word appended
1658
return new StringBuffer JavaDoc(title.substring(startAt)).append(", ").append(
1659                stop).toString();
1660    }
1661
1662    /**
1663     * Return the index of the first non-whitespace character in the String.
1664     *
1665     * @param title
1666     * @return
1667     */

1668    private static int firstWhitespace(String JavaDoc title)
1669    {
1670        return firstWhitespace(title, 0);
1671    }
1672
1673    /**
1674     * Return the index of the first non-whitespace character in the character
1675     * array.
1676     *
1677     * @param title
1678     * @return
1679     */

1680    private static int firstWhitespace(char[] title)
1681    {
1682        return firstWhitespace(title, 0);
1683    }
1684
1685    /**
1686     * Return the index of the first non-whitespace character in the String,
1687     * starting at position STARTAT.
1688     *
1689     * @param title
1690     * @param startAt
1691     * @return
1692     */

1693    private static int firstWhitespace(String JavaDoc title, int startAt)
1694    {
1695        return firstWhitespace(title.toCharArray(), startAt);
1696    }
1697
1698    /**
1699     * Return the index of the first letter or number in the character array,
1700     * starting at position STARTAT.
1701     *
1702     * @param title
1703     * @param startAt
1704     * @return
1705     */

1706    private static int firstWhitespace(char[] title, int startAt)
1707    {
1708        int first = 0;
1709
1710        for (int j = startAt; j < title.length; j++)
1711        {
1712            //if (Character.isWhitespace(title[j]))
1713
// Actually, let's skip anything that's not a letter or number
1714
if (!Character.isLetterOrDigit(title[j]))
1715            {
1716                first = j + 1;
1717
1718                continue;
1719            }
1720
1721            break;
1722        }
1723
1724        return first;
1725    }
1726}
1727
1728class BrowseCache
1729{
1730    private static Map JavaDoc tableMax = new HashMap JavaDoc();
1731
1732    private static Map JavaDoc tableSize = new HashMap JavaDoc();
1733
1734    /** log4j object */
1735    private static Logger log = Logger.getLogger(BrowseCache.class);
1736
1737    private static Map JavaDoc cache = new WeakHashMap JavaDoc();
1738
1739    // Everything in the cache is held via Weak References, and is
1740
// subject to being gc-ed at any time.
1741
// The dateCache holds normal references, so anything in it
1742
// will stay around.
1743
private static SortedMap JavaDoc dateCache = new TreeMap JavaDoc();
1744
1745    private static final int CACHE_MAXIMUM = 30;
1746
1747    /**
1748     * Look for cached Browse data corresponding to KEY.
1749     *
1750     * @param key
1751     * @return
1752     */

1753    public static BrowseInfo get(BrowseScope key)
1754    {
1755        if (log.isDebugEnabled())
1756        {
1757            log
1758                    .debug("Checking browse cache with " + cache.size()
1759                            + " objects");
1760        }
1761
1762        BrowseInfo cachedInfo = (BrowseInfo) cache.get(key);
1763
1764        try
1765        {
1766            // Index has never been calculated
1767
if (getMaximum(key) == -1)
1768            {
1769                updateIndexData(key);
1770            }
1771
1772            if (cachedInfo == null)
1773            {
1774                if (log.isDebugEnabled())
1775                {
1776                    log.debug("Not in browse cache");
1777                }
1778
1779                return null;
1780            }
1781
1782            // If we found an object, make sure that the browse indexes
1783
// have not changed.
1784
//
1785
// The granularity for this is quite large;
1786
// any change to the index and we will calculate from scratch.,
1787
// Thus, the cache works well when few changes are made, or
1788
// when changes are spaced widely apart.
1789
if (indexHasChanged(key))
1790            {
1791                if (log.isDebugEnabled())
1792                {
1793                    log.debug("Index has changed");
1794                }
1795
1796                cache.remove(key);
1797
1798                return null;
1799            }
1800        }
1801        catch (SQLException JavaDoc sqle)
1802        {
1803            if (log.isDebugEnabled())
1804            {
1805                log.debug("Caught SQLException: " + sqle, sqle);
1806            }
1807
1808            return null;
1809        }
1810
1811        // Cached object
1812
if (log.isDebugEnabled())
1813        {
1814            log.debug("Using cached browse");
1815        }
1816
1817        cachedInfo.setCached(true);
1818
1819        return cachedInfo;
1820    }
1821
1822    /**
1823     * Return true if an index has changed
1824     *
1825     * @param key
1826     * @return
1827     * @throws SQLException
1828     */

1829    public static boolean indexHasChanged(BrowseScope key) throws SQLException JavaDoc
1830    {
1831        Context context = null;
1832
1833        try
1834        {
1835            context = new Context();
1836
1837            TableRow results = countAndMax(context, key);
1838            long count = -1;
1839            int max = -1;
1840
1841            if (results != null)
1842            {
1843                // use getIntColumn for Oracle count data
1844
if ("oracle".equals(ConfigurationManager.getProperty("db.name")))
1845                {
1846                    count = results.getIntColumn("count");
1847                }
1848                else //getLongColumn works for postgres
1849
{
1850                    count = results.getLongColumn("count");
1851                }
1852                max = results.getIntColumn("max");
1853            }
1854
1855            context.complete();
1856
1857            // Same?
1858
if ((count == getCount(key)) && (max == getMaximum(key)))
1859            {
1860                return false;
1861            }
1862
1863            // Update 'em
1864
setMaximum(key, max);
1865            setCount(key, count);
1866
1867            // The index has in fact changed
1868
return true;
1869        }
1870        catch (SQLException JavaDoc sqle)
1871        {
1872            if (context != null)
1873            {
1874                context.abort();
1875            }
1876
1877            throw sqle;
1878        }
1879    }
1880
1881    /**
1882     * Compute and save the values for the number of values in the index, and
1883     * the maximum such value.
1884     *
1885     * @param key
1886     */

1887    public static void updateIndexData(BrowseScope key)
1888    {
1889        Context context = null;
1890
1891        try
1892        {
1893            context = new Context();
1894
1895            TableRow results = countAndMax(context, key);
1896            long count = -1;
1897            int max = -1;
1898
1899            if (results != null)
1900            {
1901                //use getIntColumn for Oracle count data
1902
if ("oracle".equals(ConfigurationManager.getProperty("db.name")))
1903                {
1904                    count = results.getIntColumn("count");
1905                }
1906                else //getLongColumn works for postgres
1907
{
1908                    count = results.getLongColumn("count");
1909                }
1910                max = results.getIntColumn("max");
1911            }
1912
1913            context.complete();
1914
1915            setMaximum(key, max);
1916            setCount(key, count);
1917        }
1918        catch (Exception JavaDoc e)
1919        {
1920            if (context != null)
1921            {
1922                context.abort();
1923            }
1924
1925            e.printStackTrace();
1926        }
1927    }
1928
1929    /**
1930     * Retrieve the values for count and max
1931     *
1932     * @param context
1933     * @param scope
1934     * @return
1935     * @throws SQLException
1936     */

1937    public static TableRow countAndMax(Context context, BrowseScope scope)
1938            throws SQLException JavaDoc
1939    {
1940        // The basic idea here is that we'll check an indexes
1941
// maximum id and its count: if the maximum id has changed,
1942
// then there are new values, and if the count has changed,
1943
// then some items may have been removed. We assume that
1944
// values never change.
1945
String JavaDoc sql = new StringBuffer JavaDoc().append(
1946                "select count({0}) as count, max({0}) as max from ").append(
1947                BrowseTables.getTable(scope)).append(
1948                Browse.getScopeClause(scope, "where")).toString();
1949
1950        // Format it to use the correct columns
1951
String JavaDoc countColumn = BrowseTables.getIndexColumn(scope);
1952        Object JavaDoc[] args = new Object JavaDoc[] { countColumn, countColumn };
1953        String JavaDoc SQL = MessageFormat.format(sql, args);
1954
1955        // Run the query
1956
if (log.isDebugEnabled())
1957        {
1958            log.debug("Running SQL to check whether index has changed: \""
1959                    + SQL + "\"");
1960        }
1961        
1962        return DatabaseManager.querySingle(context, SQL);
1963    }
1964
1965    /**
1966     * Add info to cache, using key.
1967     *
1968     * @param key
1969     * @param info
1970     */

1971    public static void add(BrowseScope key, BrowseInfo info)
1972    {
1973        // Don't bother caching browses with no results, they are
1974
// fairly cheap to calculate
1975
if (info.getResultCount() == 0)
1976        {
1977            return;
1978        }
1979
1980        // Add the info to the cache
1981
// Since the object is passed in to us (and thus the caller
1982
// may change it), we make a copy.
1983
cache.put(key.clone(), info);
1984
1985        // Make sure the date cache is current
1986
cleanDateCache();
1987
1988        // Save a new entry into the date cache
1989
dateCache.put(new java.util.Date JavaDoc(), key);
1990    }
1991
1992    /**
1993     * Remove entries from the date cache
1994     */

1995    private static void cleanDateCache()
1996    {
1997        synchronized (dateCache)
1998        {
1999            // Plenty of room!
2000
if (dateCache.size() < CACHE_MAXIMUM)
2001            {
2002                return;
2003            }
2004
2005            // Remove the oldest object
2006
dateCache.remove(dateCache.firstKey());
2007        }
2008    }
2009
2010    /**
2011     * Return the maximum value
2012     *
2013     * @param scope
2014     * @return
2015     */

2016    private static int getMaximum(BrowseScope scope)
2017    {
2018        String JavaDoc table = BrowseTables.getTable(scope);
2019        Integer JavaDoc value = (Integer JavaDoc) tableMax.get(table);
2020
2021        return (value == null) ? (-1) : value.intValue();
2022    }
2023
2024    private static long getCount(BrowseScope scope)
2025    {
2026        String JavaDoc table = BrowseTables.getTable(scope);
2027        Long JavaDoc value = (Long JavaDoc) tableSize.get(table);
2028
2029        return (value == null) ? (-1) : value.longValue();
2030    }
2031
2032    private static void setMaximum(BrowseScope scope, int max)
2033    {
2034        String JavaDoc table = BrowseTables.getTable(scope);
2035        tableMax.put(table, new Integer JavaDoc(max));
2036    }
2037
2038    private static void setCount(BrowseScope scope, long count)
2039    {
2040        String JavaDoc table = BrowseTables.getTable(scope);
2041        tableSize.put(table, new Long JavaDoc(count));
2042    }
2043}
2044
2045// Encapsulates browse table info:
2046
// * Each scope and browsetype has a corresponding table or view
2047
// * Each browse table or view has a value column
2048
// * Some of the browse tables are true tables, others are views.
2049
// The index maintenance code needs to know the true tables.
2050
// The true tables have index columns, which can be used for caching
2051

2052class BrowseTables
2053{
2054    private static final String JavaDoc[] BROWSE_TABLES = new String JavaDoc[] {
2055            "Communities2Item", "ItemsByAuthor", "ItemsByDate",
2056            "ItemsByDateAccessioned", "ItemsByTitle", "ItemsBySubject" };
2057
2058    /**
2059     * Return the browse tables. This only returns true tables, views are
2060     * ignored.
2061     *
2062     * @return
2063     */

2064    public static String JavaDoc[] tables()
2065    {
2066        return BROWSE_TABLES;
2067    }
2068
2069    /**
2070     * Return the browse table or view for scope.
2071     *
2072     * @param scope
2073     * @return
2074     */

2075    public static String JavaDoc getTable(BrowseScope scope)
2076    {
2077        int browseType = scope.getBrowseType();
2078        boolean isCommunity = scope.isCommunityScope();
2079        boolean isCollection = scope.isCollectionScope();
2080
2081        if ((browseType == Browse.AUTHORS_BROWSE)
2082                || (browseType == Browse.ITEMS_BY_AUTHOR_BROWSE))
2083        {
2084            if (isCommunity)
2085            {
2086                return "CommunityItemsByAuthor";
2087            }
2088
2089            if (isCollection)
2090            {
2091                return "CollectionItemsByAuthor";
2092            }
2093
2094            return "ItemsByAuthor";
2095        }
2096
2097        if (browseType == Browse.ITEMS_BY_TITLE_BROWSE)
2098        {
2099            if (isCommunity)
2100            {
2101                return "CommunityItemsByTitle";
2102            }
2103
2104            if (isCollection)
2105            {
2106                return "CollectionItemsByTitle";
2107            }
2108
2109            return "ItemsByTitle";
2110        }
2111
2112        if (browseType == Browse.ITEMS_BY_DATE_BROWSE)
2113        {
2114            if (isCommunity)
2115            {
2116                return "CommunityItemsByDate";
2117            }
2118
2119            if (isCollection)
2120            {
2121                return "CollectionItemsByDate";
2122            }
2123
2124            return "ItemsByDate";
2125        }
2126
2127        if ((browseType == Browse.SUBJECTS_BROWSE)
2128                || (browseType == Browse.ITEMS_BY_SUBJECT_BROWSE))
2129        {
2130            if (isCommunity)
2131            {
2132                return "CommunityItemsBySubject";
2133            }
2134
2135            if (isCollection)
2136            {
2137                return "CollectionItemsBySubject";
2138            }
2139
2140            return "ItemsBySubject";
2141        }
2142
2143        throw new IllegalArgumentException JavaDoc(
2144                "No table for browse and scope combination");
2145    }
2146
2147    /**
2148     * Return the name of the column that holds the index.
2149     *
2150     * @param scope
2151     * @return
2152     */

2153    public static String JavaDoc getIndexColumn(BrowseScope scope)
2154    {
2155        int browseType = scope.getBrowseType();
2156
2157        if (browseType == Browse.AUTHORS_BROWSE)
2158        {
2159            return "items_by_author_id";
2160        }
2161
2162        if (browseType == Browse.ITEMS_BY_AUTHOR_BROWSE)
2163        {
2164            return "items_by_author_id";
2165        }
2166
2167        if (browseType == Browse.ITEMS_BY_DATE_BROWSE)
2168        {
2169            return "items_by_date_id";
2170        }
2171
2172        if (browseType == Browse.ITEMS_BY_TITLE_BROWSE)
2173        {
2174            return "items_by_title_id";
2175        }
2176        
2177        if (browseType == Browse.SUBJECTS_BROWSE)
2178        {
2179            return "items_by_subject_id";
2180        }
2181
2182        if (browseType == Browse.ITEMS_BY_SUBJECT_BROWSE)
2183        {
2184            return "items_by_subject_id";
2185        }
2186
2187        throw new IllegalArgumentException JavaDoc("Unknown browse type: " + browseType);
2188    }
2189
2190    /**
2191     * Return the name of the column that holds the Browse value (the title,
2192     * author, date, etc).
2193     *
2194     * @param scope
2195     * @return
2196     */

2197    public static String JavaDoc getValueColumn(BrowseScope scope)
2198    {
2199        int browseType = scope.getBrowseType();
2200
2201        if (browseType == Browse.AUTHORS_BROWSE)
2202        {
2203            return "sort_author";
2204        }
2205
2206        if (browseType == Browse.ITEMS_BY_AUTHOR_BROWSE)
2207        {
2208            return "sort_author";
2209        }
2210
2211        if (browseType == Browse.ITEMS_BY_DATE_BROWSE)
2212        {
2213            return "date_issued";
2214        }
2215
2216        // Note that we use the normalized form of the title
2217
if (browseType == Browse.ITEMS_BY_TITLE_BROWSE)
2218        {
2219            return "sort_title";
2220        }
2221
2222        if (browseType == Browse.SUBJECTS_BROWSE)
2223        {
2224            return "sort_subject";
2225        }
2226
2227        if (browseType == Browse.ITEMS_BY_SUBJECT_BROWSE)
2228        {
2229            return "sort_subject";
2230        }
2231
2232        throw new IllegalArgumentException JavaDoc("Unknown browse type: " + browseType);
2233    }
2234}
2235
Popular Tags