Project

General

Profile

1
package eu.dnetlib.oai.parser;
2

    
3
import java.io.IOException;
4
import java.time.LocalDate;
5
import java.time.LocalDateTime;
6
import java.time.ZoneId;
7
import java.time.format.DateTimeFormatter;
8
import java.util.Date;
9
import java.util.List;
10

    
11
import com.google.common.collect.Lists;
12
import com.mongodb.BasicDBObject;
13
import com.mongodb.BasicDBObjectBuilder;
14
import eu.dnetlib.index.parse.Relation;
15
import eu.dnetlib.index.parse.Relations;
16
import eu.dnetlib.oai.conf.OAIConfigurationReader;
17
import eu.dnetlib.rmi.provision.OaiPublisherRuntimeException;
18
import org.apache.commons.lang3.StringUtils;
19
import org.apache.commons.logging.Log;
20
import org.apache.commons.logging.LogFactory;
21
import org.bson.conversions.Bson;
22
import org.bson.types.ObjectId;
23
import org.z3950.zing.cql.*;
24

    
25
/**
26
 * Instances of this class parse query string into mongo DBObject query.
27
 *
28
 * @author alessia
29
 */
30
public class MongoQueryParser {
31

    
32
	private static final Log log = LogFactory.getLog(MongoQueryParser.class); // NOPMD by marko on 11/24/08 5:02 PM
33

    
34
	/**
35
	 * Parses the given query string into a mongo DBObject.
36
	 *
37
	 * @param query
38
	 *            String to parse
39
	 * @return DBObject corresponding to the query string
40
	 */
41
	public Bson parse(final String query) {
42
		log.debug("PARSING: " + query);
43
		if (StringUtils.isBlank(query)) { return new BasicDBObject(); }
44
		final Bson parsed = toMongo(query);
45
		log.debug(parsed);
46
		return parsed;
47
	}
48

    
49
	private Bson toMongo(final String query) {
50
		final CQLParser parser = new CQLParser();
51
		CQLNode root;
52
		try {
53
			root = parser.parse(query);
54
			return this.toMongo(root);
55
		} catch (final CQLParseException e) {
56
			throw new OaiPublisherRuntimeException(e);
57
		} catch (final IOException e) {
58
			throw new OaiPublisherRuntimeException(e);
59
		}
60
	}
61

    
62
	private Bson toMongo(final CQLNode node) {
63
		if (node instanceof CQLTermNode) { return doTranslate((CQLTermNode) node); }
64
		if (node instanceof CQLBooleanNode) { return doTranslate((CQLBooleanNode) node); }
65

    
66
		throw new RuntimeException("error choice for CQLNode " + node.getClass());
67
	}
68

    
69
	private Bson doTranslate(final CQLTermNode termNode) {
70
		if (termNode.getTerm().equals("*")) { return new BasicDBObject(); }
71
		final String relation = termNode.getRelation().getBase();
72
		final Relation rel = Relations.get(relation);
73
		return this.handleRelationNode(rel, termNode);
74
	}
75

    
76
	private Bson handleRelationNode(final Relation rel, final CQLTermNode termNode) {
77
		final BasicDBObject mongoQueryObject = new BasicDBObject();
78
		final String term = termNode.getTerm();
79
		final String indexName = termNode.getIndex();
80
		Object termObj = term;
81
		if (indexName.equals("_id")) {
82
			termObj = new ObjectId(term);
83
		} else {
84
			if (indexName.equals(OAIConfigurationReader.DATESTAMP_FIELD) || indexName.equals(OAIConfigurationReader.LAST_COLLECTION_DATE_FIELD)) {
85
				// termObj = this.parseDate(term);
86
				final OAIDate termDate = this.parseDate(term);
87
				return handleDateRelationNode(indexName, rel, termDate);
88
			}
89
		}
90
		switch (rel) {
91
		case EQUAL:
92
		case EXACT:
93
			mongoQueryObject.put(indexName, termObj);
94
			break;
95
		case NOT:
96
			mongoQueryObject.put(indexName, new BasicDBObject("$ne", termObj));
97
			break;
98
		case GT:
99
			mongoQueryObject.put(indexName, new BasicDBObject("$gt", termObj));
100
			break;
101
		case GTE:
102
			mongoQueryObject.put(indexName, new BasicDBObject("$gte", termObj));
103
			break;
104
		case LT:
105
			mongoQueryObject.put(indexName, new BasicDBObject("$lt", termObj));
106
			break;
107
		case LTE:
108
			mongoQueryObject.put(indexName, new BasicDBObject("$lte", termObj));
109
			break;
110
		default:
111
			throw new OaiPublisherRuntimeException("Can't parse query: relation " + rel + " not supported!");
112
		}
113
		return mongoQueryObject;
114
	}
115

    
116
	/**
117
	 * The construction of the query changes based on the granularity of the date to handle.
118
	 * <p>
119
	 * If the date has yyyy-MM-ddThh:mm:ssZ granularity we have to create a range query because in mongo we have milliseconds, hence an
120
	 * exact match will return nothing.
121
	 * </p>
122
	 * <p>
123
	 * If the date has yyyy-MM-dd granularity then we have to trick the query. If we are interested in the date 2013-10-28, the date has
124
	 * been converted into 2013-10-28T00:00:00Z : if we ask for datestamp = 2013-10-28T00:00:00Z, we'll get nothing: we have to ask for
125
	 * records whose day is the one specified by the date.
126
	 * <p>
127
	 * </p>
128
	 *
129
	 * @param indexName
130
	 * @param rel
131
	 * @param date
132
	 * @return
133
	 */
134
	private Bson handleDateRelationNode(final String indexName, final Relation rel, final OAIDate date) {
135
		final BasicDBObject mongoQueryObject = new BasicDBObject();
136

    
137
		LocalDateTime fromDate = date.date;
138
		switch (rel) {
139
		case EQUAL:
140
		case EXACT:
141
			if (date.onlyDate) {
142

    
143
				final LocalDateTime endDate = date.date.plusDays(1);
144
				mongoQueryObject.put(indexName, BasicDBObjectBuilder.start("$gte", fromDate.toLocalDate()).append("$lt", endDate.toLocalDate()).get());
145
			} else {
146
				final LocalDateTime endDate = date.date.plusSeconds(1);
147
				mongoQueryObject.put(indexName, BasicDBObjectBuilder.start("$gte", fromDate.toLocalDate()).append("$lt", endDate.toLocalDate()).get());
148
			}
149
			break;
150
		case NOT:
151
			mongoQueryObject.put(indexName, new BasicDBObject("$ne", fromDate.toLocalDate()));
152
			break;
153
		case GT:
154
			mongoQueryObject.put(indexName, new BasicDBObject("$gt", fromDate.toLocalDate()));
155
			break;
156
		case GTE:
157
			mongoQueryObject.put(indexName, new BasicDBObject("$gte", fromDate.toLocalDate()));
158
			break;
159
		case LT:
160
			mongoQueryObject.put(indexName, new BasicDBObject("$lt", fromDate.toLocalDate()));
161
			break;
162
		case LTE:
163
			/*
164
			 * If the request is date <= YYYY-MM-DD then we need to change the date. The parseDate returned YYYY-MM-DDT00:00:00Z, but we
165
			 * need YYYY-MM-DDT23:59:59Z. To simplify we can add one day and perform < instead of <=.
166
			 */
167
			if (date.onlyDate) {
168
				fromDate = date.date.plusDays(1);
169
				mongoQueryObject.put(indexName, new BasicDBObject("$lt", Date.from(fromDate.atZone(ZoneId.systemDefault()).toInstant())));
170
			} else {
171
				mongoQueryObject.put(indexName, new BasicDBObject("$lte", Date.from(fromDate.atZone(ZoneId.systemDefault()).toInstant())));
172
			}
173
			break;
174
		default:
175
			throw new OaiPublisherRuntimeException("Can't parse query: relation " + rel + " not supported!");
176
		}
177
		return mongoQueryObject;
178
	}
179

    
180
	private Bson doTranslate(final CQLBooleanNode node) {
181
		if (node instanceof CQLAndNode) { return getBooleanQuery("$and", node); }
182
		if (node instanceof CQLOrNode) { return getBooleanQuery("$or", node); }
183
		if (node instanceof CQLNotNode) { return getNotQuery((CQLNotNode) node); }
184
		throw new RuntimeException("error choice for CQLBooleanNode " + node.getClass());
185
	}
186

    
187
	private Bson getBooleanQuery(final String mongoOperator, final CQLBooleanNode node) {
188
		final Bson left = this.toMongo(node.left);
189
		final Bson right = this.toMongo(node.right);
190
		final BasicDBObject opQuery = new BasicDBObject();
191
		final List<Bson> termList = Lists.newArrayList(left, right);
192
		opQuery.put(mongoOperator, termList);
193
		return opQuery;
194
	}
195

    
196
	private Bson getNotQuery(final CQLNotNode node) {
197
		final Bson left = this.toMongo(node.left);
198
		final Bson right = this.toMongo(node.right);
199
		final Bson notRight = new BasicDBObject("$not", right);
200
		final BasicDBObject andQuery = new BasicDBObject();
201
		final List<Bson> termList = Lists.newArrayList(left, notRight);
202
		andQuery.put("$and", termList);
203
		return andQuery;
204
	}
205

    
206
	private OAIDate parseDate(final String date) {
207

    
208
		final DateTimeFormatter dateNoTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
209
		final DateTimeFormatter iso8601NoMsTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
210
		final DateTimeFormatter iso8601Formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'.'SSS'Z'");
211
		OAIDate res = null;
212
		try {
213
			log.debug("Using default " + iso8601Formatter.getClass());
214

    
215
			final LocalDateTime dt = LocalDateTime.parse(date, iso8601Formatter);
216
			res = new OAIDate(dt, false);
217
		} catch (final Exception e) {
218
			try {
219
				log.debug("Switching to ISO with no millisecond date formatter: yyyy-MM-dd'T'HH:mm:ssZ");
220
				final LocalDateTime dt = LocalDateTime.parse(date, iso8601NoMsTimeFormatter);
221
				res = new OAIDate(dt, false);
222
			} catch (final Exception ex) {
223
				log.debug("Switching to simple date formatter: yyyy-MM-dd");
224
				final LocalDateTime dt = LocalDate.parse(date, dateNoTimeFormatter).atTime(0, 0);
225
				res = new OAIDate(dt, true);
226
			}
227
		}
228
		return res;
229
	}
230

    
231
	class OAIDate {
232

    
233
		LocalDateTime date;
234
		boolean onlyDate;
235

    
236
		OAIDate(final LocalDateTime date, final boolean onlyDate) {
237
			this.date = date;
238
			this.onlyDate = onlyDate;
239
		}
240

    
241
	}
242

    
243
}
(1-1/2)