converter) {
83 | BinaryTimeSeries series = converter.to(ts);
84 | Document document = new Document();
85 |
86 | series.getFields().entrySet().forEach(entry -> {
87 |
88 | if (entry.getValue() instanceof Number) {
89 | handleNumbers(document, entry.getKey(), entry.getValue());
90 | } else if (entry.getValue() instanceof String || entry.getValue() instanceof byte[]) {
91 | handleStringsAndBytes(document, entry.getKey(), entry.getValue());
92 | } else if (entry.getValue() instanceof Collection || entry.getValue() instanceof Object[]) {
93 | handleArraysAndIterable(document, entry.getKey(), entry.getValue());
94 | } else {
95 | LOGGER.debug("Field {} could not be handled. Type is not supported", entry);
96 | }
97 | });
98 | return document;
99 | }
100 |
101 | /**
102 | * Tries to cast field value (object) to an array or iterable.
103 | * If the field value is not an array or iterable then the method ignores the field.
104 | *
105 | * If the value is an array or iterable than the value is warped into a matching lucene field (Field for String,
106 | * StoredField for byte[]) and added to the lucene document.
107 | *
108 | * @param document the lucene document to add the number
109 | * @param fieldName the field name
110 | * @param fieldValue the field value
111 | */
112 | private static void handleArraysAndIterable(Document document, String fieldName, Object fieldValue) {
113 |
114 | //assign the value as it is modified below
115 | Object modifiedFieldValue = fieldValue;
116 |
117 | //If have an array, simple convert it into an list.
118 | if (fieldValue != null && fieldValue.getClass().isArray()) {
119 | modifiedFieldValue = Arrays.asList((Object[]) fieldValue);
120 | }
121 | //Handle all iterable data types
122 | if (modifiedFieldValue instanceof Iterable) {
123 | Iterable objects = (Iterable) modifiedFieldValue;
124 |
125 | int fieldCounter = 0;
126 | String modifiedFieldName = fieldName + ChronixLuceneStorageConstants.MULTI_VALUE_FIELD_DELIMITER;
127 | for (Object o : objects) {
128 | fieldCounter++;
129 | handleNumbers(document, modifiedFieldName + fieldCounter, o);
130 | handleStringsAndBytes(document, modifiedFieldName + fieldCounter, o);
131 | }
132 | }
133 | }
134 |
135 | /**
136 | * Tries to cast field value (object) to a string or byte[].
137 | * If the field value is not a string or a byte[] then the method ignores the field.
138 | *
139 | * If the value is a string or byte[] than the value is warped into a matching lucene field (Field for String,
140 | * StoredField for byte[]) and added to the lucene document.
141 | *
142 | * @param document the lucene document to add the number
143 | * @param fieldName the field name
144 | * @param fieldValue the field value
145 | */
146 | private static void handleStringsAndBytes(Document document, String fieldName, Object fieldValue) {
147 | if (fieldValue instanceof String) {
148 | document.add(new Field(fieldName, fieldValue.toString(), TextField.TYPE_STORED));
149 | } else if (fieldValue instanceof byte[]) {
150 | document.add(new StoredField(fieldName, new BytesRef((byte[]) fieldValue)));
151 | }
152 | }
153 |
154 | /**
155 | * Tries to cast field value (object) to a number (double, integer, float, long).
156 | * If the field value is not a number then method ignores the field.
157 | *
158 | * If the value is a number than the value is warped into a matching lucene field (IntField, DoubleField, ...)
159 | * and added to the lucene document.
160 | *
161 | * @param document the lucene document to add the number
162 | * @param fieldName the field name
163 | * @param fieldValue the field value
164 | */
165 | private static void handleNumbers(Document document, String fieldName, Object fieldValue) {
166 | if (fieldValue instanceof Double) {
167 | document.add(new StoredField(fieldName, Double.parseDouble(fieldValue.toString())));
168 | } else if (fieldValue instanceof Integer) {
169 | document.add(new StoredField(fieldName, Integer.parseInt(fieldValue.toString())));
170 | } else if (fieldValue instanceof Float) {
171 | document.add(new StoredField(fieldName, Float.parseFloat(fieldValue.toString())));
172 | } else if (fieldValue instanceof Long) {
173 | document.add(new StoredField(fieldName, Long.parseLong(fieldValue.toString())));
174 | } else {
175 | LOGGER.warn("Cloud not extract value from field {} with value {}", fieldName, fieldValue);
176 | }
177 |
178 | }
179 |
180 | }
181 |
--------------------------------------------------------------------------------
/chronix-storage/src/main/java/de/qaware/chronix/lucene/client/stream/date/DateQueryParser.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 QAware GmbH
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, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package de.qaware.chronix.lucene.client.stream.date;
17 |
18 |
19 | import org.apache.commons.lang3.StringUtils;
20 |
21 | import java.text.ParseException;
22 | import java.time.Instant;
23 | import java.util.HashMap;
24 | import java.util.Map;
25 | import java.util.regex.Pattern;
26 |
27 | /**
28 | * This class is used to first, transform queries like start:NOW-30DAYS
29 | * in expressions like 'NOW as long + 30 Days as long' and second,
30 | * to build matching range queries on our time series documents.
31 | * The current queries are supported:
32 | *
33 | * - end:47859 AND start:4578965
34 | * - end:2015-11-25T12:06:57.330Z OR start:2015-12-25T12:00:00.000Z
35 | * - start:NOW-30DAYS AND stop:NOW+30DAYS
36 | *
37 | * @author f.lautenschlager
38 | */
39 | public class DateQueryParser {
40 |
41 | private final String[] dateFields;
42 |
43 | private final Pattern solrDateMathPattern;
44 | private final Pattern instantDatePattern;
45 |
46 | /**
47 | * Constructs a date query parser
48 | *
49 | * @param dateFields - the date fields
50 | */
51 | public DateQueryParser(String[] dateFields) {
52 | this.dateFields = dateFields.clone();
53 | this.solrDateMathPattern = Pattern.compile(".*(NOW|DAY|MONTH|YEAR).*");
54 | this.instantDatePattern = Pattern.compile("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z");
55 | }
56 |
57 | /**
58 | * Converts the term for the date fields into an numeric representation.
59 | *
60 | * [0] -> numeric value for date field [0]
61 | *
62 | * [1] -> numeric value for date field [1]
63 | *
64 | * If the query does not contain a date field the value is represented as -1.
65 | *
66 | * @param query the user defined solr query
67 | * @return an array containing numeric representations of the date fields
68 | * @throws ParseException if the date term is not a numeric or solr date expression
69 | */
70 | public long[] getNumericQueryTerms(String query) throws ParseException {
71 | long[] result = new long[dateFields.length];
72 | for (int i = 0; i < dateFields.length; i++) {
73 | if (query.contains(dateFields[i])) {
74 | String dateField = dateFields[i];
75 | String dateTerm = getTokenTerm(query, dateField);
76 | result[i] = getNumberRepresentation(dateTerm);
77 | } else {
78 | result[i] = -1;
79 | }
80 | }
81 |
82 | return result;
83 | }
84 |
85 | /**
86 | * Replaces the date fields with range queries.
87 | *
88 | * @param query the plain user query
89 | * @return an enriched plain solr query
90 | * @throws ParseException if there are characters that can not be parsed
91 | */
92 | public String replaceRangeQueryTerms(String query) throws ParseException {
93 | Map replacements = new HashMap<>();
94 |
95 | String queryWithPlaceholders = markQueryWithPlaceholders(query, replacements);
96 | return replacePlaceholders(queryWithPlaceholders, replacements);
97 | }
98 |
99 | /**
100 | * Converts the given date term into a numeric representation
101 | *
102 | * @param dateTerm the date term, e.g, start:NOW+30DAYS
103 | * @return the long representation of the date term
104 | * @throws ParseException if the date term could not be evaluated
105 | */
106 | private long getNumberRepresentation(String dateTerm) throws ParseException {
107 | long numberRepresentation;
108 | if (StringUtils.isNumeric(dateTerm)) {
109 | numberRepresentation = Long.valueOf(dateTerm);
110 | } else if (solrDateMathPattern.matcher(dateTerm).matches()) {
111 | numberRepresentation = parseDateTerm(dateTerm);
112 | } else if (instantDatePattern.matcher(dateTerm).matches()) {
113 | numberRepresentation = Instant.parse(dateTerm).toEpochMilli();
114 | } else {
115 | throw new ParseException("Could not parse date representation '" + dateTerm + "'", 0);
116 | }
117 | return numberRepresentation;
118 | }
119 |
120 | /**
121 | * Replaces the placeholders with concrete values
122 | *
123 | * @param query the query with placeholders
124 | * @param replacements the replacements
125 | * @return a query with concrete values
126 | */
127 | private String replacePlaceholders(String query, Map replacements) {
128 | String resultQuery = query;
129 | for (Map.Entry entry : replacements.entrySet()) {
130 | resultQuery = resultQuery.replace(entry.getKey(), entry.getValue());
131 | }
132 | return resultQuery;
133 | }
134 |
135 | /**
136 | * @param query the origin query
137 | * @param replacements a map for to put in the replacements
138 | * @return a query with placeholders and the matching replacements
139 | * @throws ParseException if the date term could not be parsed
140 | */
141 | private String markQueryWithPlaceholders(String query, Map replacements) throws ParseException {
142 | String placeHolderQuery = query;
143 | for (int i = 0; i < dateFields.length; i++) {
144 | String dateField = dateFields[i];
145 |
146 | if (placeHolderQuery.contains(dateField)) {
147 | String dateTerm = getTokenTerm(placeHolderQuery, dateField);
148 | long numberRepresentation = getNumberRepresentation(dateTerm);
149 | String rangeQuery = getDateRangeQuery(numberRepresentation, dateField);
150 | placeHolderQuery = placeHolderQuery.replace(dateField + dateTerm, keyPart(i));
151 |
152 | //add the placeholders
153 | replacements.put(keyPart(i), rangeQuery);
154 | }
155 | }
156 | return placeHolderQuery;
157 | }
158 |
159 | /**
160 | * Important: The end of an term is marked by an " "
161 | *
162 | * @param query the origin query
163 | * @param startToken the start token
164 | * @return the term for the start token
165 | */
166 | private String getTokenTerm(String query, String startToken) {
167 | int tokenLength = startToken.length();
168 | int index = query.indexOf(startToken);
169 | int stopIndex = query.indexOf(' ', index);
170 |
171 | if (stopIndex > -1) {
172 | return query.substring(index + tokenLength, stopIndex);
173 |
174 | }
175 |
176 | return query.substring(index + tokenLength);
177 | }
178 |
179 | /**
180 | * Parses the given date query term into a date representation
181 | *
182 | * @param dateQueryTerm the date query term as string
183 | * @return the milliseconds since 1970 of the given dateQueryTerm
184 | * @throws ParseException if the term could not be parsed
185 | */
186 | private long parseDate(String dateQueryTerm) throws ParseException {
187 | return new DateMathParser().parseMath(dateQueryTerm).getTime();
188 | }
189 |
190 | /**
191 | * Builds a range query that
192 | *
193 | * @param value - the date value as long
194 | * @param field - the date field (start or end)
195 | * @return a solr range query
196 | */
197 | private String getDateRangeQuery(long value, String field) {
198 |
199 | if ("start:".equals(field)) {
200 | //We don`t need documents, that have and end before our start
201 | // q = -end[* TO (START-1)]
202 | return "-end:[* TO " + (value - 1) + "]";
203 | } else {
204 | //We don`t need documents, that have and start after our end
205 | // q = -start[* TO (START-1)]
206 | return "-start:[" + (value - 1) + " TO *]";
207 | }
208 |
209 | }
210 |
211 |
212 | private String keyPart(int i) {
213 | return "key-" + i;
214 | }
215 |
216 | /**
217 | * Parses a solr date to long representation
218 | *
219 | * @param term the solr date term (NOW + 30 DAYS)
220 | * @return the term as long
221 | * @throws ParseException if the term could not be parsed
222 | */
223 | private long parseDateTerm(String term) throws ParseException {
224 | String dateTerm = term.replace("NOW", "+0MILLISECOND");
225 | return parseDate(dateTerm);
226 | }
227 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------
/chronix-storage/src/main/java/de/qaware/chronix/lucene/client/stream/date/DateMathParser.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 QAware GmbH
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, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package de.qaware.chronix.lucene.client.stream.date;
17 |
18 |
19 | import java.text.ParseException;
20 | import java.util.*;
21 | import java.util.regex.Pattern;
22 |
23 | /**
24 | * A Simple Utility class for parsing "math" like strings relating to Dates.
25 | *
26 | *
27 | * The basic syntax support addition, subtraction and rounding at various
28 | * levels of granularity (or "units"). Commands can be chained together
29 | * and are parsed from left to right. '+' and '-' denote addition and
30 | * subtraction, while '/' denotes "round". Round requires only a unit, while
31 | * addition/subtraction require an integer value and a unit.
32 | * Command strings must not include white space, but the "No-Op" command
33 | * (empty string) is allowed....
34 | *
35 | *
36 | *
37 | * /HOUR
38 | * ... Round to the start of the current hour
39 | * /DAY
40 | * ... Round to the start of the current day
41 | * +2YEARS
42 | * ... Exactly two years in the future from now
43 | * -1DAY
44 | * ... Exactly 1 day prior to now
45 | * /DAY+6MONTHS+3DAYS
46 | * ... 6 months and 3 days in the future from the start of
47 | * the current day
48 | * +6MONTHS+3DAYS/DAY
49 | * ... 6 months and 3 days in the future from now, rounded
50 | * down to nearest day
51 | *
52 | *
53 | *
54 | * (Multiple aliases exist for the various units of time (ie:
55 | * MINUTE and MINUTES; MILLI,
56 | * MILLIS, MILLISECOND, and
57 | * MILLISECONDS.) The complete list can be found by
58 | * inspecting the keySet of {@link #CALENDAR_UNITS})
59 | *
60 | *
61 | *
62 | * All commands are relative to a "now" which is fixed in an instance of
63 | * DateMathParser such that
64 | * p.parseMath("+0MILLISECOND").equals(p.parseMath("+0MILLISECOND"))
65 | * no matter how many wall clock milliseconds elapse between the two
66 | * distinct calls to parse (Assuming no other thread calls
67 | * "setNow" in the interim). The default value of 'now' is
68 | * the time at the moment the DateMathParser instance is
69 | * constructed, unless overridden by the {CommonParams#NOW NOW}
70 | * request param.
71 | *
72 | *
73 | *
74 | * All commands are also affected to the rules of a specified {@link TimeZone}
75 | * (including the start/end of DST if any) which determine when each arbitrary
76 | * day starts. This not only impacts rounding/adding of DAYs, but also
77 | * cascades to rounding of HOUR, MIN, MONTH, YEAR as well. The default
78 | * TimeZone used is UTC unless overridden by the
79 | * {CommonParams#TZ TZ}
80 | * request param.
81 | *
82 | */
83 | public final class DateMathParser {
84 |
85 | private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
86 |
87 | /**
88 | * Default TimeZone for DateMath rounding (UTC)
89 | */
90 | private static final TimeZone DEFAULT_MATH_TZ = UTC;
91 | /**
92 | * Default Locale for DateMath rounding (Locale.ROOT)
93 | */
94 | private static final Locale DEFAULT_MATH_LOCALE = Locale.ROOT;
95 |
96 | /**
97 | * A mapping from (uppercased) String labels idenyifying time units,
98 | * to the corresponding Calendar constant used to set/add/roll that unit
99 | * of measurement.
100 | *
101 | *
102 | * A single logical unit of time might be represented by multiple labels
103 | * for convenience (ie: DATE==DAY,
104 | * MILLI==MILLISECOND)
105 | *
106 | *
107 | * @see Calendar
108 | */
109 | private static final Map CALENDAR_UNITS = makeUnitsMap();
110 |
111 | private static Pattern splitter = Pattern.compile("\\b|(?<=\\d)(?=\\D)");
112 |
113 | private TimeZone zone;
114 | private Locale loc;
115 | private Date now;
116 |
117 | /**
118 | * Default constructor that assumes UTC should be used for rounding unless
119 | * otherwise specified in the SolrRequestInfo
120 | *
121 | * @see #DEFAULT_MATH_LOCALE
122 | */
123 | public DateMathParser() {
124 | this(null, DEFAULT_MATH_LOCALE);
125 |
126 | }
127 |
128 | /**
129 | * @param tz The TimeZone used for rounding (to determine when hours/days begin). If null, then this method defaults to the value dicated by the SolrRequestInfo if it
130 | * exists -- otherwise it uses UTC.
131 | * @param l The Locale used for rounding (to determine when weeks begin). If null, then this method defaults to en_US.
132 | * @see #DEFAULT_MATH_TZ
133 | * @see #DEFAULT_MATH_LOCALE
134 | * @see Calendar#getInstance(TimeZone, Locale)
135 | */
136 | public DateMathParser(TimeZone tz, Locale l) {
137 | if (null == l) {
138 | loc = DEFAULT_MATH_LOCALE;
139 | } else {
140 | loc = l;
141 | }
142 |
143 | if (null == tz) {
144 | zone = DEFAULT_MATH_TZ;
145 | } else {
146 | zone = tz;
147 | }
148 | }
149 |
150 |
151 | /**
152 | * @see #CALENDAR_UNITS
153 | */
154 | private static Map makeUnitsMap() {
155 |
156 | // NOTE: consciously choosing not to support WEEK at this time,
157 | // because of complexity in rounding down to the nearest week
158 | // arround a month/year boundry.
159 | // (Not to mention: it's not clear what people would *expect*)
160 | //
161 | // If we consider adding some time of "week" support, then
162 | // we probably need to change "Locale loc" to default to something
163 | // from a param via SolrRequestInfo as well.
164 |
165 | Map units = new HashMap<>(13);
166 | units.put("YEAR", Calendar.YEAR);
167 | units.put("YEARS", Calendar.YEAR);
168 | units.put("MONTH", Calendar.MONTH);
169 | units.put("MONTHS", Calendar.MONTH);
170 | units.put("DAY", Calendar.DATE);
171 | units.put("DAYS", Calendar.DATE);
172 | units.put("DATE", Calendar.DATE);
173 | units.put("HOUR", Calendar.HOUR_OF_DAY);
174 | units.put("HOURS", Calendar.HOUR_OF_DAY);
175 | units.put("MINUTE", Calendar.MINUTE);
176 | units.put("MINUTES", Calendar.MINUTE);
177 | units.put("SECOND", Calendar.SECOND);
178 | units.put("SECONDS", Calendar.SECOND);
179 | units.put("MILLI", Calendar.MILLISECOND);
180 | units.put("MILLIS", Calendar.MILLISECOND);
181 | units.put("MILLISECOND", Calendar.MILLISECOND);
182 | units.put("MILLISECONDS", Calendar.MILLISECOND);
183 |
184 | return units;
185 | }
186 |
187 | /**
188 | * Modifies the specified Calendar by "adding" the specified value of units
189 | *
190 | * @throws IllegalArgumentException if unit isn't recognized.
191 | * @see #CALENDAR_UNITS
192 | */
193 | private static void add(Calendar c, int val, String unit) {
194 | Integer uu = CALENDAR_UNITS.get(unit);
195 | if (null == uu) {
196 | throw new IllegalArgumentException("Adding Unit not recognized: " + unit);
197 | }
198 | c.add(uu.intValue(), val);
199 | }
200 |
201 | /**
202 | * Modifies the specified Calendar by "rounding" down to the specified unit
203 | *
204 | * @throws IllegalArgumentException if unit isn't recognized.
205 | * @see #CALENDAR_UNITS
206 | */
207 | private static void round(Calendar c, String unit) {
208 | Integer uu = CALENDAR_UNITS.get(unit);
209 | if (null == uu) {
210 | throw new IllegalArgumentException("Rounding Unit not recognized: " + unit);
211 | }
212 | int u = uu;
213 |
214 | switch (u) {
215 |
216 | case Calendar.YEAR:
217 | c.clear(Calendar.MONTH);
218 | /* fall through */
219 | case Calendar.MONTH:
220 | c.clear(Calendar.DAY_OF_MONTH);
221 | c.clear(Calendar.DAY_OF_WEEK);
222 | c.clear(Calendar.DAY_OF_WEEK_IN_MONTH);
223 | c.clear(Calendar.DAY_OF_YEAR);
224 | c.clear(Calendar.WEEK_OF_MONTH);
225 | c.clear(Calendar.WEEK_OF_YEAR);
226 | /* fall through */
227 | case Calendar.DATE:
228 | c.clear(Calendar.HOUR_OF_DAY);
229 | c.clear(Calendar.HOUR);
230 | c.clear(Calendar.AM_PM);
231 | /* fall through */
232 | case Calendar.HOUR_OF_DAY:
233 | c.clear(Calendar.MINUTE);
234 | /* fall through */
235 | case Calendar.MINUTE:
236 | c.clear(Calendar.SECOND);
237 | /* fall through */
238 | case Calendar.SECOND:
239 | c.clear(Calendar.MILLISECOND);
240 | break;
241 | default:
242 | throw new IllegalStateException("No logic for rounding value (" + u + ") " + unit);
243 | }
244 |
245 | }
246 |
247 | /**
248 | * @return the current date
249 | */
250 | private Date getNow() {
251 | if (now == null) {
252 | // fall back to current time if no request info set
253 | now = new Date();
254 | }
255 | return (Date) now.clone();
256 | }
257 |
258 | /**
259 | * Parses a string of commands relative "now" are returns the resulting Date.
260 | *
261 | * @return the resulting date
262 | * @throws ParseException positions in ParseExceptions are token positions, not character positions.
263 | */
264 | @SuppressWarnings("all") // The class is copied from solr codebase
265 | public Date parseMath(String math) throws ParseException {
266 |
267 | Calendar cal = Calendar.getInstance(zone, loc);
268 | cal.setTime(getNow());
269 |
270 | /* check for No-Op */
271 | if (0 == math.length()) {
272 | return cal.getTime();
273 | }
274 |
275 | String[] ops = splitter.split(math);
276 | int pos = 0;
277 | while (pos < ops.length) {
278 |
279 | if (1 != ops[pos].length()) {
280 | throw new ParseException("Multi character command found: \"" + ops[pos] + "\"", pos);
281 | }
282 | char command = ops[pos++].charAt(0);
283 |
284 | switch (command) {
285 | case '/':
286 | if (ops.length < pos + 1) {
287 | throw new ParseException("Need a unit after command: \"" + command + "\"", pos);
288 | }
289 | try {
290 | round(cal, ops[pos++]);
291 | } catch (IllegalArgumentException e) {
292 | throw new ParseException("Unit not recognized: \"" + ops[pos - 1] + "\"", pos - 1);
293 | }
294 | break;
295 | case '+': /* fall through */
296 | case '-':
297 | if (ops.length < pos + 2) {
298 | throw new ParseException("Need a value and unit for command: \"" + command + "\"", pos);
299 | }
300 | int val;
301 | try {
302 | val = Integer.valueOf(ops[pos++]);
303 | } catch (NumberFormatException e) {
304 | throw new ParseException("Not a Number: \"" + ops[pos - 1] + "\"", pos - 1);
305 | }
306 | if ('-' == command) {
307 | val = 0 - val;
308 | }
309 | try {
310 | String unit = ops[pos++];
311 | add(cal, val, unit);
312 | } catch (IllegalArgumentException e) {
313 | throw new ParseException("Unit not recognized: \"" + ops[pos - 1] + "\"", pos - 1);
314 | }
315 | break;
316 | default:
317 | throw new ParseException("Unrecognized command: \"" + command + "\"", pos - 1);
318 | }
319 | }
320 |
321 | return cal.getTime();
322 | }
323 |
324 |
325 | }
326 |
--------------------------------------------------------------------------------