001 /*
002 *
003 * $Revision: 15594 $ $Date: 2009-07-23 13:15:39 +0200 (Thu, 23 Jul 2009) $
004 *
005 * This file is part of *** M y C o R e ***
006 * See http://www.mycore.de/ for details.
007 *
008 * This program is free software; you can use it, redistribute it
009 * and / or modify it under the terms of the GNU General Public License
010 * (GPL) as published by the Free Software Foundation; either version 2
011 * of the License or (at your option) any later version.
012 *
013 * This program is distributed in the hope that it will be useful, but
014 * WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program, in a file called gpl.txt or license.txt.
020 * If not, write to the Free Software Foundation Inc.,
021 * 59 Temple Place - Suite 330, Boston, MA 02111-1307 USA
022 */
023
024 package org.mycore.services.fieldquery;
025
026 import java.util.ArrayList;
027 import java.util.Arrays;
028 import java.util.Collections;
029 import java.util.Comparator;
030 import java.util.HashMap;
031 import java.util.Iterator;
032 import java.util.List;
033 import java.util.Map;
034 import java.util.Random;
035
036 import org.jdom.Document;
037 import org.jdom.Element;
038 import org.mycore.common.MCRConstants;
039
040 /**
041 * This class represents the results of a query performed by MCRSearcher.
042 * Searchers add the hits using the addHit() method. Clients can get the hits,
043 * sort the entries and do merge/and/or operations on two different result sets.
044 *
045 * Searches may add the same hit (hit with the same ID) more than once. If the
046 * hit already is contained in the result set, the data of both objects is
047 * merged.
048 *
049 * @see MCRHit
050 *
051 * @author Arne Seifert
052 * @author Frank L\u00fctzenkirchen
053 * @author Jens Kupferschmidt
054 * @version $Revision: 15594 $ $Date: 2009-07-23 13:15:39 +0200 (Thu, 23 Jul 2009) $
055 */
056 public class MCRResults implements Iterable<MCRHit> {
057 /** The list of MCRHit objects */
058 protected ArrayList<MCRHit> hits = new ArrayList<MCRHit>();
059
060 /** The state of the connection */
061 private HashMap<String, String> hostconnection = new HashMap<String, String>();
062
063 /**
064 * A map containing MCRHit IDs used for and/or operations on two different
065 * MCRResult objects
066 */
067 protected HashMap<String, MCRHit> map = new HashMap<String, MCRHit>();
068
069 /** If true, this results are already sorted */
070 private boolean isSorted = false;
071
072 /** The unique ID of this result set */
073 private String id;
074
075 private static Random random = new Random(System.currentTimeMillis());
076
077 /**
078 * Creates a new, empty MCRResults.
079 */
080 public MCRResults() {
081 id = Long.toString(random.nextLong(), 36) + Long.toString(System.currentTimeMillis(), 36);
082 }
083
084 /**
085 * Returns the unique ID of this result set
086 *
087 * @return the unique ID of this result set
088 */
089 public String getID() {
090 return id;
091 }
092
093 /**
094 * Adds a hit. If there is already a hit with the same ID, the sort data and
095 * meta data of both hits are merged and the merged hit replaces the
096 * existing hit.
097 *
098 * @param hit
099 * the MCRHit to add
100 */
101 public void addHit(MCRHit hit) {
102 String key = hit.getKey();
103 MCRHit existing = getHit(key);
104
105 if (existing == null) {
106 // This is a new entry with new ID
107 hits.add(hit);
108 map.put(key, hit);
109 } else {
110 // Merge data of existing hit with new one with the same ID
111 existing.merge(hit);
112 }
113 }
114
115 /**
116 * Gets a single MCRHit. As long as isSorted() returns false, the order of
117 * the hits is natural order.
118 *
119 * @param i
120 * the position of the hit.
121 * @return the hit at this position, or null if position is out of bounds
122 */
123 public MCRHit getHit(int i) {
124 if ((i >= 0) && (i < hits.size())) {
125 return hits.get(i);
126 }
127 return null;
128 }
129
130 /**
131 * Returns the MCRHit with the given key, if it is in this results.
132 *
133 * @param key
134 * the key of the hit
135 * @return the MCRHit, if it exists
136 */
137 protected MCRHit getHit(String key) {
138 return map.get(key);
139 }
140
141 /**
142 * Returns the number of hits currently in this results
143 *
144 * @return the number of hits
145 */
146 public int getNumHits() {
147 return hits.size();
148 }
149
150 /**
151 * Cuts the result list to the given maximum size, if more hits are present.
152 *
153 * @param maxResults
154 * the number of results to be left
155 */
156 public void cutResults(int maxResults) {
157 while ((hits.size() > maxResults) && (maxResults > 0)) {
158 MCRHit hit = hits.remove(hits.size() - 1);
159 map.remove(hit.getKey());
160 }
161 }
162
163 /**
164 * The searcher must set this to true, if the hits already have been added
165 * in sorted order.
166 *
167 * @param value
168 * true, if sorted, false otherwise
169 */
170 public void setSorted(boolean value) {
171 isSorted = value;
172 }
173
174 /**
175 * Returns true if this result list is currently sorted
176 *
177 * @return true if this result list is currently sorted
178 */
179 public boolean isSorted() {
180 return isSorted;
181 }
182
183 /**
184 * Sorts this results by the given sort criteria.
185 *
186 * @param sortByList
187 * a List of MCRSortBy objects
188 */
189 public void sortBy(final List<MCRSortBy> sortByList) {
190 Collections.sort(this.hits, new Comparator<MCRHit>() {
191 public int compare(MCRHit a, MCRHit b) {
192 int result = 0;
193
194 for (int i = 0; (result == 0) && (i < sortByList.size()); i++) {
195 MCRSortBy sortBy = sortByList.get(i);
196 if (sortBy.getSortOrder() == MCRSortBy.ASCENDING)
197 result = a.compareTo(sortBy.getField(), b);
198 else
199 result = b.compareTo(sortBy.getField(), a);
200 }
201
202 return result;
203 }
204 });
205 setSorted(true);
206 }
207
208 /**
209 * Returns a XML element containing hits and their data
210 *
211 * @param min
212 * the position of the first hit to include in output
213 * @param max
214 * the position of the last hit to include in output
215 * @return a 'results' element with attributes 'sorted' and 'numHits' and
216 * hit child elements
217 */
218 public Element buildXML(int min, int max) {
219 Element results = new Element("results", MCRConstants.MCR_NAMESPACE);
220 results.setAttribute("id", getID());
221 results.setAttribute("sorted", Boolean.toString(isSorted()));
222 results.setAttribute("numHits", String.valueOf(getNumHits()));
223
224 for (Map.Entry<String, String> entry : hostconnection.entrySet()) {
225 Element connection = new Element("hostconnection", MCRConstants.MCR_NAMESPACE);
226 connection.setAttribute("host", entry.getKey());
227 String msg = entry.getValue();
228 if (msg == null)
229 msg = "";
230 connection.setAttribute("message", msg);
231 if (msg.length() == 0) {
232 connection.setAttribute("connection", "true");
233 } else {
234 connection.setAttribute("connection", "false");
235 }
236 results.addContent(connection);
237 }
238
239 for (int i = min; i <= max; i++)
240 results.addContent(getHit(i).buildXML());
241
242 return results;
243 }
244
245 /**
246 * Returns a XML element containing all hits and their data
247 *
248 * @return a 'results' element with attributes 'sorted' and 'numHits' and
249 * hit child elements
250 */
251 public Element buildXML() {
252 return buildXML(0, getNumHits() - 1);
253 }
254
255 /**
256 * Merges the hits from a remote query to this results
257 *
258 * @param doc
259 * the results from the remote query as XML document
260 * @param hostAlias
261 * the alias of the host where the hits come from
262 * @return the number of hits added
263 */
264 protected int merge(Document doc, String hostAlias) {
265 Element xml = doc.getRootElement();
266 int numHitsBefore = this.getNumHits();
267 int numRemoteHits = Integer.parseInt(xml.getAttributeValue("numHits"));
268
269 @SuppressWarnings("unchecked")
270 List<Element> connectionList = xml.getChildren("hostconnection", MCRConstants.MCR_NAMESPACE);
271 for (Iterator<Element> it = connectionList.iterator(); it.hasNext();) {
272 Element connectionElement = it.next();
273 String conKey = connectionElement.getAttributeValue("host");
274 String conValue = connectionElement.getAttributeValue("message");
275 hostconnection.put(conKey, conValue);
276 }
277
278 @SuppressWarnings("unchecked")
279 List<Element> hitList = xml.getChildren("hit", MCRConstants.MCR_NAMESPACE);
280 hits.ensureCapacity(numHitsBefore + numRemoteHits);
281 for (Iterator<Element> it = hitList.iterator(); it.hasNext();) {
282 Element hitElement = it.next();
283 MCRHit hit = MCRHit.parseXML(hitElement, hostAlias);
284 hits.add(hit);
285 map.put(hit.getKey(), hit);
286 }
287 return this.getNumHits() - numHitsBefore;
288 }
289
290 public String toString() {
291 StringBuffer sb = new StringBuffer();
292 sb.append("---- MCRResults ----");
293 sb.append("\nNumHits = ").append(this.getNumHits());
294 for (int i = 0; i < hits.size(); i++)
295 sb.append(hits.get(i));
296 return sb.toString();
297 }
298
299 /**
300 * Does a logical and of this results hits and other results hits. The hits
301 * that are contained in both results are kept, the others are removed from
302 * this results list. The data of common hits is combined from both result
303 * lists.
304 *
305 * @param other
306 * the other result lists
307 */
308 public static MCRResults intersect(MCRResults... others) {
309 //check if result is empty
310 for (MCRResults other : others) {
311 // x AND {} is always {}
312 if (other.getNumHits() == 0) {
313 return new MCRResults();
314 }
315 }
316 final MCRResults firstResult = others[0];
317 MCRResults totalResult = new MCRResults();
318 final List<MCRResults> subResultList = Arrays.asList(others).subList(1, others.length);
319 //merge everything together
320 for (MCRHit hit : firstResult) {
321 boolean complete = true;
322 final String key = hit.getKey();
323 for (MCRResults other : subResultList) {
324 MCRHit otherHit = other.getHit(key);
325 if (otherHit == null) {
326 complete = false;
327 break;
328 }
329 hit.merge(otherHit);
330 }
331 if (complete)
332 totalResult.addHit(hit);
333 }
334 return totalResult;
335 }
336
337 /**
338 * Adds all hits of another result list that are not yet in this result
339 * list. Combines the MCRHit data of both result lists.
340 *
341 * @param other
342 * the other result lists
343 */
344 public static MCRResults union(MCRResults... others) {
345 MCRResults totalResult = new MCRResults();
346 for (MCRResults other : others) {
347 for (MCRHit hit : other)
348 totalResult.addHit(hit);
349 }
350 return totalResult;
351 }
352
353 public Iterator<MCRHit> iterator() {
354 return hits.iterator();
355 }
356
357 /**
358 * Set the state of the connection of a host alias.
359 *
360 * @param host
361 * the host alias
362 * @param msg
363 * the exception message of the connection or an empty string
364 */
365 public void setHostConnection(String host, String msg) {
366 if (msg == null)
367 msg = "";
368 hostconnection.put(host, msg);
369 }
370
371 /**
372 * returns false if {@link #addHit(MCRHit)} and {@link #merge(Document, String)} are safe operations.
373 * @return
374 */
375 public boolean isReadonly() {
376 return false;
377 }
378 }