Project

General

Profile

1
package eu.dnetlib.functionality.cql.mongo;
2

    
3
import java.io.IOException;
4
import java.util.List;
5

    
6
import com.google.common.collect.Lists;
7
import com.mongodb.BasicDBObject;
8
import com.mongodb.BasicDBObjectBuilder;
9
import eu.dnetlib.functionality.cql.parse.Relation;
10
import eu.dnetlib.functionality.cql.parse.Relations;
11
import org.apache.commons.lang3.StringUtils;
12
import org.apache.commons.logging.Log;
13
import org.apache.commons.logging.LogFactory;
14
import org.bson.conversions.Bson;
15
import org.bson.types.ObjectId;
16
import org.joda.time.DateTime;
17
import org.joda.time.format.DateTimeFormat;
18
import org.joda.time.format.DateTimeFormatter;
19
import org.joda.time.format.ISODateTimeFormat;
20
import org.z3950.zing.cql.*;
21

    
22
/**
23
 * Created by claudio on 12/09/16.
24
 */
25
public class MongoCqlTranslator {
26

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

    
29
	//field names containing dates for the OAI publisher. See also OAICOnfigurationReader constants that are not visible here.
30
	//TODO: Generalize
31
	private final List<String> dateFields = Lists.newArrayList("datestamp", "lastCollectionDate");
32
	/**
33
	 * Parses the given query string into a mongo DBObject.
34
	 *
35
	 * @param query
36
	 *            String to parse
37
	 * @return DBObject corresponding to the query string
38
	 */
39
	public static Bson toMongo(final String query) throws CQLParseException, IOException {
40
		log.debug("PARSING: " + query);
41
		if (StringUtils.isBlank(query)) return new BasicDBObject();
42
		final Bson parsed = new MongoCqlTranslator().doParse(query);
43
		log.debug(parsed);
44
		return parsed;
45
	}
46

    
47
	private Bson doParse(final String query) throws IOException, CQLParseException {
48
		CQLParser parser = new CQLParser();
49
		CQLNode root;
50
		root = parser.parse(query);
51
		return doParse(root);
52
	}
53

    
54
	private Bson doParse(final CQLNode node) throws CQLParseException {
55
		if (node instanceof CQLTermNode) return doTranslate((CQLTermNode) node);
56
		if (node instanceof CQLBooleanNode) return doTranslate((CQLBooleanNode) node);
57

    
58
		throw new RuntimeException("error choice for CQLNode " + node.getClass());
59
	}
60

    
61
	private Bson doTranslate(final CQLTermNode termNode) throws CQLParseException {
62
		if (termNode.getTerm().equals("*")  && (termNode.getIndex().equals("*") || termNode.getIndex().equals("cql.serverChoice"))) return new BasicDBObject();
63
		String relation = termNode.getRelation().getBase();
64
		Relation rel = Relations.get(relation);
65
		return this.handleRelationNode(rel, termNode);
66
	}
67

    
68
	private Bson handleRelationNode(final Relation rel, final CQLTermNode termNode) throws CQLParseException {
69
		BasicDBObject mongoQueryObject = new BasicDBObject();
70
		String term = termNode.getTerm();
71
		String indexName = termNode.getIndex();
72
		Object termObj = term;
73
		if (dateFields.contains(indexName)){
74
			OAIDate termDate = this.parseDate(term);
75
			return handleDateRelationNode(indexName, rel, termDate);
76
		}
77
		if (indexName.equals("_id")) {
78
			termObj = new ObjectId(term);
79
		}
80
		switch (rel) {
81
		case EQUAL:
82
		case EXACT:
83
			if(!term.equals("*")) {
84
				mongoQueryObject.put(indexName, termObj);
85
			}
86
			else{
87
				//special case to handle exist queries such as fieldname = * --> qty: { $exists: true}
88
				mongoQueryObject.put(indexName, new BasicDBObject("$exists", true));
89
			}
90
			break;
91
		case NOT:
92
			mongoQueryObject.put(indexName, new BasicDBObject("$ne", termObj));
93
			break;
94
		case GT:
95
			mongoQueryObject.put(indexName, new BasicDBObject("$gt", termObj));
96
			break;
97
		case GTE:
98
			mongoQueryObject.put(indexName, new BasicDBObject("$gte", termObj));
99
			break;
100
		case LT:
101
			mongoQueryObject.put(indexName, new BasicDBObject("$lt", termObj));
102
			break;
103
		case LTE:
104
			mongoQueryObject.put(indexName, new BasicDBObject("$lte", termObj));
105
			break;
106
		default:
107
			throw new CQLParseException("Can't parse query: relation " + rel + " not supported!");
108
		}
109
		return mongoQueryObject;
110
	}
111

    
112
	private Bson doTranslate(final CQLBooleanNode node) throws CQLParseException {
113
		if (node instanceof CQLAndNode) return getBooleanQuery("$and", node);
114
		if (node instanceof CQLOrNode) return getBooleanQuery("$or", node);
115
		if (node instanceof CQLNotNode) return getNotQuery((CQLNotNode) node);
116
		throw new RuntimeException("error choice for CQLBooleanNode " + node.getClass());
117
	}
118

    
119
	private Bson getBooleanQuery(final String mongoOperator, final CQLBooleanNode node) throws CQLParseException {
120
		Bson left = doParse(node.left);
121
		Bson right = doParse(node.right);
122
		BasicDBObject opQuery = new BasicDBObject();
123
		List<Bson> termList = Lists.newArrayList(left, right);
124
		opQuery.put(mongoOperator, termList);
125
		return opQuery;
126
	}
127

    
128
	private Bson getNotQuery(final CQLNotNode node) throws CQLParseException {
129
		Bson left = doParse(node.left);
130
		Bson right = doParse(node.right);
131
		Bson notRight = new BasicDBObject("$not", right);
132
		BasicDBObject andQuery = new BasicDBObject();
133
		List<Bson> termList = Lists.newArrayList(left, notRight);
134
		andQuery.put("$and", termList);
135
		return andQuery;
136
	}
137

    
138
	/**
139
	 * The construction of the query changes based on the granularity of the date to handle.
140
	 * <p>
141
	 * 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
142
	 * exact match will return nothing.
143
	 * </p>
144
	 * <p>
145
	 * 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
146
	 * 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
147
	 * records whose day is the one specified by the date.
148
	 *
149
	 * </p>
150
	 *
151
	 * @param indexName
152
	 * @param rel
153
	 * @param date
154
	 * @return
155
	 */
156
	private Bson handleDateRelationNode(final String indexName, final Relation rel, final OAIDate date) {
157
		BasicDBObject mongoQueryObject = new BasicDBObject();
158
		DateTime fromDate = date.date;
159
		switch (rel) {
160
		case EQUAL:
161
		case EXACT:
162
			if (date.onlyDate) {
163
				DateTime endDate = date.date.plusDays(1);
164
				mongoQueryObject.put(indexName, BasicDBObjectBuilder.start("$gte", fromDate.toDate()).append("$lt", endDate.toDate()).get());
165
			} else {
166
				DateTime endDate = date.date.plusSeconds(1);
167
				mongoQueryObject.put(indexName, BasicDBObjectBuilder.start("$gte", fromDate.toDate()).append("$lt", endDate.toDate()).get());
168
			}
169
			break;
170
		case NOT:
171
			mongoQueryObject.put(indexName, new BasicDBObject("$ne", fromDate.toDate()));
172
			break;
173
		case GT:
174
			mongoQueryObject.put(indexName, new BasicDBObject("$gt", fromDate.toDate()));
175
			break;
176
		case GTE:
177
			mongoQueryObject.put(indexName, new BasicDBObject("$gte", fromDate.toDate()));
178
			break;
179
		case LT:
180
			mongoQueryObject.put(indexName, new BasicDBObject("$lt", fromDate.toDate()));
181
			break;
182
		case LTE:
183
			/*
184
			If the request is date <= YYYY-MM-DD then we need to change the date.
185
			The parseDate returned YYYY-MM-DDT00:00:00Z, but we need YYYY-MM-DDT23:59:59Z.
186
			To simplify we can add one day and perform < instead of <=.
187
			 */
188
			if (date.onlyDate) {
189
				fromDate = date.date.plusDays(1);
190
				mongoQueryObject.put(indexName, new BasicDBObject("$lt", fromDate.toDate()));
191
			} else {
192
				mongoQueryObject.put(indexName, new BasicDBObject("$lte", fromDate.toDate()));
193
			}
194
			break;
195
		default:
196
			throw new RuntimeException("Can't parse query: relation " + rel + " not supported!");
197
		}
198
		return mongoQueryObject;
199
	}
200

    
201

    
202
	private OAIDate parseDate(final String date) {
203
		DateTimeFormatter dateNoTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd").withZoneUTC();
204
		DateTimeFormatter iso8601NoMsTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ssZ").withZoneUTC();
205
		DateTimeFormatter iso8601Formatter = ISODateTimeFormat.dateTime().withZoneUTC();
206
		OAIDate res = null;
207
		try {
208
			log.debug("Using default " + iso8601Formatter.getClass());
209
			DateTime dt = iso8601Formatter.parseDateTime(date);
210
			res = new OAIDate(dt, false);
211
		} catch (Exception e) {
212
			try {
213
				log.debug("Switching to ISO with no millisecond date formatter: yyyy-MM-dd'T'HH:mm:ssZ");
214
				DateTime dt = iso8601NoMsTimeFormatter.parseDateTime(date);
215
				res = new OAIDate(dt, false);
216
			} catch (Exception ex) {
217
				log.debug("Switching to simple date formatter: yyyy-MM-dd");
218
				DateTime dt = dateNoTimeFormatter.parseDateTime(date);
219
				res = new OAIDate(dt, true);
220
			}
221
		}
222
		return res;
223
	}
224

    
225
	class OAIDate {
226

    
227
		DateTime date;
228
		boolean onlyDate;
229

    
230
		OAIDate(final DateTime date, final boolean onlyDate) {
231
			this.date = date;
232
			this.onlyDate = onlyDate;
233
		}
234

    
235
	}
236

    
237

    
238
}
    (1-1/1)