1   package com.atlassian.bonnie;
2   
3   import org.apache.lucene.document.DateField;
4   import org.apache.lucene.document.DateTools;
5   import org.apache.lucene.document.Document;
6   import org.apache.lucene.document.Field;
7   import org.apache.lucene.index.Term;
8   import org.apache.lucene.search.*;
9   import org.slf4j.Logger;
10  import org.slf4j.LoggerFactory;
11  
12  import java.text.ParseException;
13  import java.util.*;
14  import java.util.concurrent.atomic.AtomicInteger;
15  
16  /**
17   * Utilites for Lucene-related functionality in applications. Be sure to deprecate before removing anything here,
18   * as it may be used in applications and plugins.
19   */
20  public class LuceneUtils
21  {
22      private static final Logger log = LoggerFactory.getLogger(LuceneUtils.class);
23      private static final AtomicInteger invalidDateWarningCount = new AtomicInteger();
24      private static final int INVALID_DATE_MAX_WARNINGS = 20;
25  
26      /**
27       * Converts a Date into a String using the Lucene index format, with a millisecond resolution.
28       *
29       * @see DateTools#dateToString
30       */
31      public static String dateToString(Date date)
32      {
33          return DateTools.dateToString(date, DateTools.Resolution.MILLISECOND);
34      }
35  
36      /**
37       * Converts a String from the Lucene index format into a Date. Attempts conversion using both
38       * new and old date formats. If the String is blank or null, returns a new Date object.
39       * <p/>
40       * If the string is in the old format, this method will log a warning a limited number of times
41       * per application run to recommend that the user upgrades the index format.
42       */
43      public static Date stringToDate(String s)
44      {
45          if (s != null && s.trim().length() > 0)
46          {
47              try
48              {
49                  return DateTools.stringToDate(s);
50              }
51              catch (ParseException e)
52              {
53                  int currentErrorCount = invalidDateWarningCount.get();
54                  if (currentErrorCount <= INVALID_DATE_MAX_WARNINGS) {
55                      invalidDateWarningCount.incrementAndGet();
56                      log.warn("Unable to parse a date found in the index because it uses an invalid encoding. Rebuilding the search index is recommended.");
57                      if (currentErrorCount == INVALID_DATE_MAX_WARNINGS) {
58                          log.warn("Suppressing more warnings about invalid dates until the application is restarted.");
59                      }
60                  }
61                  return DateField.stringToDate(s);
62              }
63  
64          }
65          return new Date();
66      }
67  
68      public static Query buildSingleFieldSingleValueTermQuery(String field, String query)
69      {
70          return buildSingleFieldMultiValueTermQuery(field, Collections.singletonList(query), true);
71      }
72  
73      /**
74       * Builds a query that checks that the field contains all or some of the values specified.
75       * You can set this behaviour by adjusting the value of the addQuery parameter
76       *
77       * @param andQuery - set to true if you require the value of the field to match ALL values
78       */
79      public static Query buildSingleFieldMultiValueTermQuery(String field, Collection values, boolean andQuery)
80      {
81          BooleanQuery query = new BooleanQuery();
82  
83          BooleanClause.Occur occur = andQuery ? BooleanClause.Occur.MUST : BooleanClause.Occur.SHOULD;
84  
85          for (Iterator iterator = values.iterator(); iterator.hasNext();)
86          {
87              String value = (String) iterator.next();
88              query.add(new TermQuery(new Term(field, value)), occur);
89          }
90  
91          return query;
92      }
93  
94      public static Query buildSingleFieldMultiValuePrefixQuery(String field, Collection values, boolean andQuery)
95      {
96          BooleanQuery query = new BooleanQuery();
97  
98          BooleanClause.Occur occur = andQuery ? BooleanClause.Occur.MUST : BooleanClause.Occur.SHOULD;
99  
100         for (Iterator iterator = values.iterator(); iterator.hasNext();)
101         {
102             String value = (String) iterator.next();
103             query.add(new PrefixQuery(new Term(field, value)), occur);
104         }
105 
106         return query;
107     }
108 
109     /**
110      * Builds a Map representation of a Document.
111      * This allows dot-style notation in Velocity to be retained for both object- and map-backed
112      * representations.
113      * <p/>
114      * Dotted fields are split into maps that will lead to the eventual value.
115      * For example, content.space.name will resolve to map->map->string.
116      * This approach also assumes there is no clash between dotted keys and top-level keys,
117      * i.e. there aren't both content=foo and content.space.name=bar.
118      * </p>
119      *
120      * @param doc
121      * @return
122      */
123     public static Map buildMapFromDocument(Document doc)
124     {
125         Map result = new HashMap();
126         for (Iterator iter = doc.getFields().iterator(); iter.hasNext();)
127         {
128             Field f = (Field) iter.next();
129             String fieldname = f.name();
130             if (fieldname.indexOf('.') > -1)
131             {
132                 String[] split = fieldname.split("\\.");
133                 Map last = result;
134                 for (int i = 0; i < split.length; ++i)
135                 {
136                     String key = split[i];
137                     if (i == split.length - 1)
138                     {
139                         last.put(key, f.stringValue()); // terminal case
140                     }
141                     else
142                     {
143                         Object temp = last.get(key);
144                         if (temp != null && !(temp instanceof Map))
145                         {
146                             break;
147                         }
148 
149                         if (temp == null)
150                         {
151                             temp = new HashMap(5);
152                             last.put(key, temp);
153                         }
154                         last = (Map) temp;
155                     }
156                 }
157             }
158             else
159             {
160                 result.put(fieldname, f.stringValue());      // assume there's only one value/field
161             }
162         }
163         return result;
164     }
165 }