initial version of frontend layer
[opensuse:webpin2.git] / api / webpin.api / src / main / java / org / opensuse / webpin / api / SearchUI.java
1 /*
2  ******************************************************************************
3  *      Atos Worldline GmbH                Common Development
4  *      Pascalstrasse 19                   Phone +49 (0) 2408/148-0
5  *      D-52076 Aachen
6  *
7  *      Atos Worldline is an Atos Origin company
8  *      All rights reserved
9  *
10  ******************************************************************************
11  */
12 package org.opensuse.webpin.api;
13
14 import java.util.ArrayList;
15 import java.util.Collection;
16 import java.util.HashMap;
17 import java.util.LinkedList;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Map.Entry;
21 import java.util.regex.Matcher;
22 import java.util.regex.Pattern;
23
24 import org.apache.solr.client.solrj.SolrQuery;
25 import org.restlet.Request;
26 import org.restlet.Response;
27 import org.restlet.data.MediaType;
28 import org.restlet.data.Method;
29 import org.restlet.data.Reference;
30 import org.restlet.data.Status;
31 import org.restlet.ext.freemarker.TemplateRepresentation;
32 import org.restlet.representation.StringRepresentation;
33 import org.restlet.representation.Variant;
34 import org.restlet.resource.ResourceException;
35 import org.springframework.beans.factory.annotation.Required;
36
37 import com.google.common.base.Function;
38 import com.google.common.base.Joiner;
39 import com.google.common.collect.Lists;
40 import com.google.common.collect.Maps;
41
42 import freemarker.template.Configuration;
43
44 public class SearchUI extends SolrQueryRestlet {
45     
46     private QueryMethod queryMethod;
47     private Configuration freemarker;
48     private int maxPages = 10;
49     
50     private static final Map<String, String> DISTS;
51     static {
52         DISTS = Maps.newLinkedHashMap();
53         DISTS.put("openSUSE_11.3", "openSUSE 11.3");
54         DISTS.put("openSUSE_11.2", "openSUSE 11.2");
55         DISTS.put("openSUSE_11.1", "openSUSE 11.1");
56     }
57
58     private static final Map<String, String> ARCHS;
59     static {
60         ARCHS = Maps.newLinkedHashMap();
61         ARCHS.put("i586", "i586 (32 bit)");
62         ARCHS.put("x86_64", "x86_64 (64 bit)");
63         ARCHS.put("noarch", "noarch (any)");
64     }
65
66     private static final Map<String, String> TYPES;
67     static {
68         TYPES = Maps.newLinkedHashMap();
69         TYPES.put("lib", "libraries");
70         TYPES.put("devel", "development");
71         TYPES.put("doc", "documentation");
72         TYPES.put("lang", "localization");
73     }
74
75     private static final Pattern OBS_REPO_PATTERN = Pattern.compile("^(?:http|https|ftp)://download\\.opensuse\\.org/repositories/(.+)/(?:.+)/?$");
76     private static final Pattern OSS_REPO_PATTERN = Pattern.compile("^(?:http|https|ftp)://download\\.opensuse\\.org/distribution/(?:[^/]+)/repo/oss/?$");
77     private static final Pattern NON_OSS_REPO_PATTERN = Pattern.compile("^(?:http|https|ftp)://download\\.opensuse\\.org/distribution/(?:[^/]+)/repo/non-oss/?$");
78     private static final Map<Pattern, String> REPO_URL_PATTERNS = new HashMap<Pattern, String>() {{
79         put(OBS_REPO_PATTERN, "%s");
80         put(OSS_REPO_PATTERN, "oss");
81         put(NON_OSS_REPO_PATTERN, "non-oss");
82     }};
83     
84     private static final String[] HIGHLIGHT_FIELDS = { "name", "summary", "file", "fqfile", "description", };
85     private static final String[] POST_PROC_MULTIVALUED = { "tag" };
86     
87     private final QueryMethod highlightingQueryMethod = new QueryMethod() {
88         @Override
89         public String configure(Request request, Response response, SolrQueryRestlet rsc, SolrQuery query, List<String> filterQueries) {
90             final String name = queryMethod.configure(request, response, rsc, query, filterQueries);
91             
92             query
93             .setHighlight(true)
94             .setHighlightFragsize(300)
95             .setHighlightSnippets(3)
96             .setHighlightSimplePre("<em>")
97             .setHighlightSimplePost("</em>")
98             ;
99             for (final String highlightField : HIGHLIGHT_FIELDS) {
100                 query.addHighlightField(highlightField);
101             }
102             
103             return name;
104         }
105     };
106
107     private final QueryMethod archQueryMethod = new QueryMethod() {
108         @Override
109         public String configure(Request request, Response response, SolrQueryRestlet rsc, SolrQuery query, List<String> filterQueries) {
110             final String name = highlightingQueryMethod.configure(request, response, rsc, query, filterQueries);
111             final String arch = getStringAttribute(request, "arch", null);
112             if (arch != null && (! arch.equals("any"))) {
113                 filterQueries.add("+arch:" + arch);
114             }
115             for (final String tagParam : TYPES.keySet()) {
116                 if (getIntAttribute(request, response, tagParam, 0) == 0) {
117                         filterQueries.add("-tag:" + tagParam);
118                     }
119             }
120             return name;
121         }
122     }; 
123     
124     
125     @Override
126     public void handle(final Request request, final Response response) {
127         final Method method = request.getMethod();
128         if (method.equals(Method.GET)) {
129             get(request, response);
130         } else {
131             response.setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED, String.format("the method %s is not supported", method.getName()));
132         }
133     }
134     
135     private static final List<Variant> VARIANTS;
136     static {
137         VARIANTS = new LinkedList<Variant>();
138         VARIANTS.add(new Variant(MediaType.TEXT_HTML));
139     }
140
141     protected final void head(final Request request, final Response response) throws ResourceException {
142         final QueryResult r = query(queryMethod, request, response);
143         if (r == null) {
144             return;
145         }
146
147         if (r.results.getNumFound() < 1) {
148             response.setStatus(Status.CLIENT_ERROR_NOT_FOUND);
149         }
150
151         response.setEntity(new StringRepresentation("", MediaType.TEXT_PLAIN));
152     }
153     
154     public final void get(final Request request, final Response response) throws ResourceException {
155         final QueryResult r = query(archQueryMethod, request, response);
156         if (r == null) {
157             return;
158         }
159
160         final Map<String, Object> model = new HashMap<String, Object>();
161         
162         final List<Map<String, Object>> packages;
163         {
164             final List<Map<String, Object>> highlightedPackages;
165             {
166                 highlightedPackages = new ArrayList<Map<String,Object>>(r.results.size());
167                 for (final Map<String, Object> pkg : r.results) {
168                     final Object id = pkg.get("id");
169                     final Map<String, Object> modelPkg;
170                     {
171                         final Map<String, List<String>> pkgHighlightMap = r.highlighting.get(id);
172                         if (pkgHighlightMap != null) {
173                             modelPkg = new HashMap<String, Object>(pkg.size());
174                             for (final Entry<String, Object> e : pkg.entrySet()) {
175                                 final List<String> fieldHighlight = pkgHighlightMap.get(e.getKey());
176                                 if (fieldHighlight != null) {
177                                     modelPkg.put(e.getKey(), Joiner.on("&hellip;").join(fieldHighlight));
178                                 } else {
179                                     modelPkg.put(e.getKey(), e.getValue());
180                                 }
181                             }
182                         } else {
183                             modelPkg = pkg;
184                         }
185                     }
186                     highlightedPackages.add(modelPkg);
187                 }
188             }
189             final List<Map<String, Object>> postProcessedPackages = Lists.transform(highlightedPackages, new Function<Map<String, Object>, Map<String, Object>>() {
190                 @SuppressWarnings("unchecked")
191                                 @Override public Map<String, Object> apply(final Map<String, Object> pkg) {
192                         if (pkg.containsKey("repourl")) {
193                         final String repotag;
194                         if (pkg.get("repoid").toString().startsWith("packman-")) {
195                                 repotag = "Packman";
196                             } else {
197                                 String t = null;
198                                 for (final Entry<Pattern, String> e : REPO_URL_PATTERNS.entrySet()) {
199                                     final Matcher m = e.getKey().matcher(pkg.get("repourl").toString());
200                                     if (m.matches()) {
201                                         t = String.format(e.getValue(), m.group());
202                                         break;
203                                     }
204                                 }
205                                 repotag = t;
206                             }
207                             pkg.put("repotag", repotag);
208                     }
209                     {
210                         for (final String field : POST_PROC_MULTIVALUED) {
211                                 final Object o = pkg.get(field);
212                                 if (o != null) {
213                                         if (o instanceof Collection<?>) {
214                                                 final List<Object> list = Lists.newArrayList((Collection<Object>) o);
215                                                 pkg.put(field, list);
216                                         } else {
217                                                 pkg.put(field, Lists.newArrayList(o));
218                                         }
219                                 }
220                         }
221                     }
222                     return pkg;
223                 }
224             });
225             packages = postProcessedPackages;
226         }
227         
228         model.put("packages", packages);
229         model.put("q", r.query);
230
231         model.put("alldists", DISTS);
232         model.put("allarchs", ARCHS);
233         model.put("alltags", TYPES);
234         final List<String> tags = new LinkedList<String>();
235         for (final String tagParam : TYPES.keySet()) {
236                 final int value = getIntAttribute(request, response, tagParam, 0);
237                 model.put(tagParam, value);
238                 if (value != 0) {
239                         tags.add(tagParam);
240                 }
241         }
242         model.put("tags", tags);
243         model.put("dists", r.dists);
244         model.put("archs", r.archs);
245         
246         if (r.results.getNumFound() < 1) {
247             response.setStatus(Status.CLIENT_ERROR_NOT_FOUND);
248         }
249
250         model.put("qtime", r.qtime);
251         
252         // paging
253         final int start = (int) r.start;
254         final int total = (int) r.results.getNumFound();
255         final int rows = (int) (r.rows >= 0 ? r.rows : this.defaultRows);
256         final int pages = total > 0 ? (total / rows) + 1 : 0;
257         final int currentPage = (start / rows) + 1;
258         model.put("hits", total);
259         model.put("start", start);
260         model.put("rows", rows);
261         model.put("pages", pages);
262         model.put("currentPage", currentPage);
263         
264         // compute pages here instead of in the view layer
265         // reconstruct:
266         final String uriTemplate;
267         {
268                 final Reference ref = new Reference(request.getResourceRef().getBaseRef());
269                 ref.addQueryParameter("q", r.query);
270                 for (final String dist : r.dists) {
271                         ref.addQueryParameter("dist", dist);
272                 }
273                 for (final String arch : r.archs) {
274                         ref.addQueryParameter("arch", arch);
275                 }
276                 for (final String tag : tags) {
277                         ref.addQueryParameter(tag, "1");
278                 }
279                 ref.addQueryParameter("start", "");
280                 uriTemplate = ref.toString(true, false);
281         }
282         final int pagesToShow = pages <= maxPages ? pages : maxPages;
283         if (pagesToShow > 1) {
284             final List<Map<String, Object>> pagelist = Lists.newArrayListWithCapacity((int) pages);
285                 for (int i = 1; i <= pagesToShow; i++) {
286                         final Map<String, Object> attr = Maps.newHashMapWithExpectedSize(3);
287                         final int pageStart = (i - 1) * rows;
288                         attr.put("page", i);
289                         attr.put("url", uriTemplate + pageStart);
290                         attr.put("current", i == currentPage);
291                         pagelist.add(attr);
292                 }
293                 if (currentPage > 1) {
294                         model.put("previous", pagelist.get((currentPage - 1) - 1));
295                 }
296                 if (currentPage < pages) {
297                         model.put("next", pagelist.get((currentPage + 1) - 1));
298                 }
299                 if (pagesToShow < pages) {
300                         model.put("more", true);
301                 }
302                 model.put("pagelist", pagelist);
303         }
304         model.put("solrquery", r.solrQuery);
305         
306         response.setEntity(new TemplateRepresentation("ui.html", freemarker, model, MediaType.TEXT_HTML));
307     }
308     
309     @Required public void setFreemarker(Configuration freemarker) {
310         this.freemarker = freemarker;
311     }
312     
313     @Required public void setQueryMethod(QueryMethod queryMethod) {
314         this.queryMethod = queryMethod;
315     }
316
317 }