View Javadoc
1   /*
2    * Copyright 2012-2021 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.util;
17  
18  import static org.codelibs.core.stream.StreamUtil.split;
19  import static org.codelibs.core.stream.StreamUtil.stream;
20  
21  import java.util.Map;
22  import java.util.stream.Collectors;
23  
24  import org.codelibs.core.lang.StringUtil;
25  import org.codelibs.fess.Constants;
26  import org.codelibs.fess.entity.SearchRequestParams;
27  import org.codelibs.fess.helper.RelatedQueryHelper;
28  import org.codelibs.fess.mylasta.direction.FessConfig;
29  
30  public class QueryStringBuilder {
31  
32      private static final String OR_ALT = " || ";
33  
34      private static final String OR = " OR ";
35  
36      private static final String SPACE = " ";
37  
38      private SearchRequestParams params;
39  
40      private boolean escape = false;
41  
42      private String sortField;
43  
44      protected String quote(final String value) {
45          if (value.split("\\s").length > 1) {
46              return new StringBuilder().append('"').append(value.replace('"', ' ')).append('"').toString();
47          }
48          return value;
49      }
50  
51      protected String escapeQuery(final String value) {
52          if (!escape) {
53              return value;
54          }
55  
56          String newValue = value;
57          for (final String element : Constants.RESERVED) {
58              final String replacement = element.replaceAll("(.)", "\\\\$1");
59              newValue = newValue.replace(element, replacement);
60          }
61          return newValue;
62      }
63  
64      public String build() {
65          final FessConfig fessConfig = ComponentUtil.getFessConfig();
66          final int maxQueryLength = fessConfig.getQueryMaxLengthAsInteger();
67          final StringBuilder queryBuf = new StringBuilder(255);
68  
69          final String query = buildBaseQuery();
70          if (StringUtil.isNotBlank(query)) {
71              queryBuf.append(escapeQuery(query));
72          }
73  
74          stream(params.getExtraQueries())
75                  .of(stream -> stream.filter(q -> StringUtil.isNotBlank(q) && q.length() <= maxQueryLength).forEach(q -> {
76                      appendQuery(queryBuf, q);
77                  }));
78  
79          stream(params.getFields()).of(stream -> stream.forEach(entry -> {
80              final String key = entry.getKey();
81              final String[] values = entry.getValue();
82              if (values == null) {
83                  // nothing
84              } else if (values.length == 1) {
85                  queryBuf.append(' ').append(key).append(":\"").append(values[0]).append('\"');
86              } else if (values.length > 1) {
87                  boolean first = true;
88                  queryBuf.append(" (");
89                  for (final String value : values) {
90                      if (first) {
91                          first = false;
92                      } else {
93                          queryBuf.append(OR);
94                      }
95                      queryBuf.append(key).append(":\"").append(value).append('\"');
96                  }
97                  queryBuf.append(')');
98              }
99          }));
100 
101         final String baseQuery = queryBuf.toString().trim();
102         if (StringUtil.isBlank(sortField)) {
103             return baseQuery;
104         }
105         return baseQuery + " sort:" + sortField;
106     }
107 
108     protected void appendQuery(final StringBuilder queryBuf, final String query) {
109         String q = query;
110         for (final String s : ComponentUtil.getFessConfig().getCrawlerDocumentSpaces()) {
111             q = q.replace(s, SPACE);
112         }
113         final boolean exists = q.indexOf(OR) != -1 || q.indexOf(OR_ALT) != -1;
114         queryBuf.append(' ');
115         if (exists) {
116             queryBuf.append('(');
117         }
118         queryBuf.append(query);
119         if (exists) {
120             queryBuf.append(')');
121         }
122     }
123 
124     protected String buildBaseQuery() {
125         final StringBuilder queryBuf = new StringBuilder(255);
126         if (params.hasConditionQuery()) {
127             appendConditions(queryBuf, params.getConditions());
128         } else {
129             final String query = params.getQuery();
130             if (StringUtil.isNotBlank(query)) {
131                 if (ComponentUtil.hasRelatedQueryHelper()) {
132                     final RelatedQueryHelper relatedQueryHelper = ComponentUtil.getRelatedQueryHelper();
133                     final String[] relatedQueries = relatedQueryHelper.getRelatedQueries(query);
134                     if (relatedQueries.length == 0) {
135                         appendQuery(queryBuf, query);
136                     } else {
137                         queryBuf.append('(');
138                         queryBuf.append(quote(query));
139                         for (final String s : relatedQueries) {
140                             queryBuf.append(OR);
141                             queryBuf.append(quote(s));
142                         }
143                         queryBuf.append(')');
144                     }
145                 } else {
146                     appendQuery(queryBuf, query);
147                 }
148             }
149         }
150         return queryBuf.toString().trim();
151     }
152 
153     protected void appendConditions(final StringBuilder queryBuf, final Map<String, String[]> conditions) {
154         if (conditions == null) {
155             return;
156         }
157         final FessConfig fessConfig = ComponentUtil.getFessConfig();
158         final int maxQueryLength = fessConfig.getQueryMaxLengthAsInteger();
159 
160         stream(conditions.get(SearchRequestParams.AS_OCCURRENCE))
161                 .of(stream -> stream.filter(this::isOccurrence).findFirst().ifPresent(q -> queryBuf.insert(0, q + ":")));
162 
163         stream(conditions.get(SearchRequestParams.AS_Q)).of(stream -> stream
164                 .filter(q -> StringUtil.isNotBlank(q) && q.length() <= maxQueryLength).forEach(q -> queryBuf.append(' ').append(q)));
165         stream(conditions.get(SearchRequestParams.AS_EPQ))
166                 .of(stream -> stream.filter(q -> StringUtil.isNotBlank(q) && q.length() <= maxQueryLength)
167                         .forEach(q -> queryBuf.append(" \"").append(escape(q, "\"")).append('"')));
168         stream(conditions.get(SearchRequestParams.AS_OQ)).of(stream -> stream
169                 .filter(q -> StringUtil.isNotBlank(q) && q.length() <= maxQueryLength)
170                 .forEach(oq -> split(oq, " ")
171                         .get(s -> s.filter(StringUtil::isNotBlank).reduce((q1, q2) -> escape(q1, "(", ")") + OR + escape(q2, "(", ")")))
172                         .ifPresent(q -> {
173                             appendQuery(queryBuf, q);
174                         })));
175         stream(conditions.get(SearchRequestParams.AS_NQ))
176                 .of(stream -> stream.filter(q -> StringUtil.isNotBlank(q) && q.length() <= maxQueryLength).forEach(eq -> {
177                     final String nq =
178                             split(eq, " ").get(s -> s.filter(StringUtil::isNotBlank).map(q -> "NOT " + q).collect(Collectors.joining(" ")));
179                     queryBuf.append(' ').append(nq);
180                 }));
181         stream(conditions.get(SearchRequestParams.AS_FILETYPE))
182                 .of(stream -> stream.filter(q -> StringUtil.isNotBlank(q) && q.length() <= maxQueryLength)
183                         .forEach(q -> queryBuf.append(" filetype:\"").append(q.trim()).append('"')));
184         stream(conditions.get(SearchRequestParams.AS_SITESEARCH))
185                 .of(stream -> stream.filter(q -> StringUtil.isNotBlank(q) && q.length() <= maxQueryLength)
186                         .forEach(q -> queryBuf.append(" site:").append(q.trim())));
187         stream(conditions.get(SearchRequestParams.AS_TIMESTAMP))
188                 .of(stream -> stream.filter(q -> StringUtil.isNotBlank(q) && q.length() <= maxQueryLength)
189                         .forEach(q -> queryBuf.append(" timestamp:").append(q.trim())));
190     }
191 
192     protected boolean isOccurrence(final String value) {
193         return "allintitle".equals(value) || "allinurl".equals(value);
194     }
195 
196     protected String escape(final String q, final String... values) {
197         String value = q;
198         for (final String s : values) {
199             value = value.replace(s, "\\" + s);
200         }
201         return value;
202     }
203 
204     public QueryStringBuilder params(final SearchRequestParams params) {
205         this.params = params;
206         return this;
207     }
208 
209     public QueryStringBuilder sortField(final String sortField) {
210         this.sortField = sortField;
211         return this;
212     }
213 
214     public QueryStringBuilder escape(final boolean escape) {
215         this.escape = escape;
216         return this;
217     }
218 }