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    }