View Javadoc

1   /*
2    * Copyright (C) 2007 ETH Zurich
3    *
4    * This file is part of Fosstrak (www.fosstrak.org).
5    *
6    * Fosstrak is free software; you can redistribute it and/or
7    * modify it under the terms of the GNU Lesser General Public
8    * License version 2.1, as published by the Free Software Foundation.
9    *
10   * Fosstrak is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13   * Lesser General Public License for more details.
14   *
15   * You should have received a copy of the GNU Lesser General Public
16   * License along with Fosstrak; if not, write to the Free
17   * Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18   * Boston, MA  02110-1301  USA
19   */
20  
21  package org.fosstrak.epcis.repository.query;
22  
23  import java.net.MalformedURLException;
24  import java.net.URL;
25  import java.sql.SQLException;
26  import java.text.ParseException;
27  import java.util.ArrayList;
28  import java.util.Calendar;
29  import java.util.Collections;
30  import java.util.Comparator;
31  import java.util.GregorianCalendar;
32  import java.util.HashMap;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Set;
36  
37  import javax.servlet.ServletContext;
38  import javax.sql.DataSource;
39  import javax.xml.datatype.XMLGregorianCalendar;
40  
41  import org.fosstrak.epcis.model.ArrayOfString;
42  import org.fosstrak.epcis.model.DuplicateSubscriptionException;
43  import org.fosstrak.epcis.model.EPCISEventType;
44  import org.fosstrak.epcis.model.EventListType;
45  import org.fosstrak.epcis.model.ImplementationException;
46  import org.fosstrak.epcis.model.ImplementationExceptionSeverity;
47  import org.fosstrak.epcis.model.InvalidURIException;
48  import org.fosstrak.epcis.model.NoSuchNameException;
49  import org.fosstrak.epcis.model.NoSuchSubscriptionException;
50  import org.fosstrak.epcis.model.QueryParam;
51  import org.fosstrak.epcis.model.QueryParameterException;
52  import org.fosstrak.epcis.model.QueryParams;
53  import org.fosstrak.epcis.model.QueryResults;
54  import org.fosstrak.epcis.model.QueryResultsBody;
55  import org.fosstrak.epcis.model.QuerySchedule;
56  import org.fosstrak.epcis.model.QueryTooLargeException;
57  import org.fosstrak.epcis.model.SubscribeNotPermittedException;
58  import org.fosstrak.epcis.model.SubscriptionControls;
59  import org.fosstrak.epcis.model.SubscriptionControlsException;
60  import org.fosstrak.epcis.model.ValidationException;
61  import org.fosstrak.epcis.model.VocabularyListType;
62  import org.fosstrak.epcis.repository.EpcisConstants;
63  import org.fosstrak.epcis.repository.EpcisQueryControlInterface;
64  import org.fosstrak.epcis.repository.query.SimpleEventQueryDTO.EventQueryParam;
65  import org.fosstrak.epcis.repository.query.SimpleEventQueryDTO.Operation;
66  import org.fosstrak.epcis.repository.query.SimpleEventQueryDTO.OrderDirection;
67  import org.fosstrak.epcis.soap.DuplicateSubscriptionExceptionResponse;
68  import org.fosstrak.epcis.soap.ImplementationExceptionResponse;
69  import org.fosstrak.epcis.soap.InvalidURIExceptionResponse;
70  import org.fosstrak.epcis.soap.NoSuchNameExceptionResponse;
71  import org.fosstrak.epcis.soap.NoSuchSubscriptionExceptionResponse;
72  import org.fosstrak.epcis.soap.QueryParameterExceptionResponse;
73  import org.fosstrak.epcis.soap.QueryTooComplexExceptionResponse;
74  import org.fosstrak.epcis.soap.QueryTooLargeExceptionResponse;
75  import org.fosstrak.epcis.soap.SecurityExceptionResponse;
76  import org.fosstrak.epcis.soap.SubscribeNotPermittedExceptionResponse;
77  import org.fosstrak.epcis.soap.SubscriptionControlsExceptionResponse;
78  import org.fosstrak.epcis.soap.ValidationExceptionResponse;
79  import org.fosstrak.epcis.utils.TimeParser;
80  import org.apache.commons.collections.CollectionUtils;
81  import org.apache.commons.collections.Transformer;
82  import org.apache.commons.logging.Log;
83  import org.apache.commons.logging.LogFactory;
84  import org.w3c.dom.Element;
85  import org.w3c.dom.Node;
86  import org.w3c.dom.NodeList;
87  
88  /**
89   * EPCIS Query Operations Module implementing the SOAP/HTTP binding of the Query
90   * Control Interface. The implementation converts invocations from Axis into SQL
91   * queries and returns the results back to the requesting client through Axis.
92   * 
93   * @author David Gubler
94   * @author Alain Remund
95   * @author Arthur van Dorp
96   * @author Marco Steybe
97   */
98  public class QueryOperationsModule implements EpcisQueryControlInterface {
99  
100     private static final Log LOG = LogFactory.getLog(QueryOperationsModule.class);
101 
102     /**
103      * The version of the standard that this service is implementing.
104      */
105     private static final String STD_VERSION = "1.0";
106 
107     /**
108      * The names of all the implemented queries.
109      */
110     private static final List<String> QUERYNAMES;
111     static {
112         QUERYNAMES = new ArrayList<String>(2);
113         QUERYNAMES.add("SimpleEventQuery");
114         QUERYNAMES.add("SimpleMasterDataQuery");
115     }
116 
117     /**
118      * The version of this service implementation. The empty string indicates
119      * that the implementation implements only standard functionality with no
120      * vendor extensions.
121      */
122     private String serviceVersion = "";
123 
124     /**
125      * The maximum number of rows a query can return.
126      */
127     private int maxQueryRows;
128 
129     /**
130      * The maximum timeout to wait for a query to return.
131      */
132     private int maxQueryTime;
133 
134     // time to wait for checking trigger conditions
135     private String triggerConditionSeconds;
136     private String triggerConditionMinutes;
137 
138     private ServletContext servletContext;
139     private DataSource dataSource;
140     private QueryOperationsBackend backend;
141 
142     /**
143      * Create an SQL query string from the given query parameters.
144      * <p>
145      * Note: the CXF framework always returns an instance of org.w3c.dom.Element
146      * for the query parameter value given in the <code>queryParams</code>
147      * argument, because the spec defines this value to be of type
148      * <code>anyType</code>. CXF <i>does not</i> resolve the type of the
149      * query parameter value from the query name as Axis does! However, if the
150      * user specifies the XML type in the request, then CXF returns an instance
151      * of the corresponding type.
152      * <p>
153      * Consider the following example of a query parameter:
154      * 
155      * <pre>
156      * &lt;param&gt;
157      *   &lt;name&gt;GE_eventTime&lt;/name&gt;
158      *   &lt;value&gt;2007-07-07T07:07:07+02:00&lt;/value&gt;
159      * &lt;/param&gt;
160      * </pre>
161      * 
162      * For the query parameter value, CXF will return an instance of
163      * org.w3c.dom.Element containing the text value
164      * "2007-07-07T07:07:07+02:00". However, if the user provides the following
165      * instead, CXF will return an instance of
166      * javax.xml.datatype.XMLGregorianCalendar.
167      * 
168      * <pre>
169      * &lt;param&gt;
170      *   &lt;name&gt;GE_eventTime&lt;/name&gt;
171      *   &lt;value
172      *       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
173      *       xmlns:xs=&quot;http://www.w3.org/2001/XMLSchema&quot;
174      *       xsi:type=&quot;xs:dateTime&quot;&gt;
175      *     2007-07-07T07:07:07+02:00
176      *   &lt;/value&gt;
177      * &lt;/param&gt;
178      * </pre>
179      * 
180      * As a consequence, we always first need to check if the value is an
181      * instance of Element, and if so, we need to parse it manually according to
182      * the semantics of the parameter name.
183      * 
184      * @param queryParams
185      *            The query parameters.
186      * @param eventType
187      *            Has to be one of the four basic event types "ObjectEvent",
188      *            "AggregationEvent", "QuantityEvent", "TransactionEvent".
189      * @return The prepared sql statement.
190      * @throws SQLException
191      *             Whenever something goes wrong when querying the db.
192      * @throws QueryParameterException
193      *             If one of the given QueryParam is invalid.
194      * @throws ImplementationException
195      *             If an error in the implementation occurred.
196      */
197     private List<SimpleEventQueryDTO> constructSimpleEventQueries(final QueryParams queryParams) throws SQLException,
198             QueryParameterExceptionResponse {
199         SimpleEventQueryDTO aggrEventQuery = new SimpleEventQueryDTO(EpcisConstants.AGGREGATION_EVENT);
200         SimpleEventQueryDTO objEventQuery = new SimpleEventQueryDTO(EpcisConstants.OBJECT_EVENT);
201         SimpleEventQueryDTO quantEventQuery = new SimpleEventQueryDTO(EpcisConstants.QUANTITY_EVENT);
202         SimpleEventQueryDTO transEventQuery = new SimpleEventQueryDTO(EpcisConstants.TRANSACTION_EVENT);
203 
204         boolean includeAggrEvents = true;
205         boolean includeObjEvents = true;
206         boolean includeQuantEvents = true;
207         boolean includeTransEvents = true;
208 
209         String orderBy = null;
210         OrderDirection orderDirection = null;
211         int eventCountLimit = -1;
212         int maxEventCount = -1;
213 
214         // a sorted List of query parameter names - keeps track of the processed
215         // names in order to cope with duplicates
216         List<String> sortedParamNames = new ArrayList<String>();
217 
218         int nofEventFieldExtensions = 0;
219         for (QueryParam param : queryParams.getParam()) {
220             String paramName = param.getName();
221             Object paramValue = param.getValue();
222 
223             // check for null values
224             if (paramName == null || "".equals(paramName)) {
225                 String msg = "Missing name for a query parameter";
226                 throw queryParameterException(msg, null);
227             }
228             if (paramValue == null) {
229                 String msg = "Missing value for query parameter '" + paramName + "'";
230                 throw queryParameterException(msg, null);
231             }
232             // check if the current query parameter has already been provided
233             int index = Collections.binarySearch(sortedParamNames, paramName);
234             if (index < 0) {
235                 // we have not yet seen this query parameter name - ok
236                 sortedParamNames.add(-index - 1, paramName);
237             } else {
238                 // we have already handled this query parameter name - not ok
239                 String msg = "Query parameter '" + paramName + "' provided more than once";
240                 throw queryParameterException(msg, null);
241             }
242 
243             if (LOG.isDebugEnabled()) {
244                 LOG.debug("Handling query parameter: " + paramName);
245             }
246             try {
247                 if (paramName.equals("eventType")) {
248                     // by default all event types will be included
249                     List<String> eventTypes = parseAsArrayOfString(paramValue).getString();
250                     if (!eventTypes.isEmpty()) {
251                         // check if valid event types are provided
252                         checkEventTypes(eventTypes);
253 
254                         // check for excluded event types
255                         if (!eventTypes.contains(EpcisConstants.AGGREGATION_EVENT)) {
256                             includeAggrEvents = false;
257                         }
258                         if (!eventTypes.contains(EpcisConstants.OBJECT_EVENT)) {
259                             includeObjEvents = false;
260                         }
261                         if (!eventTypes.contains(EpcisConstants.QUANTITY_EVENT)) {
262                             includeQuantEvents = false;
263                         }
264                         if (!eventTypes.contains(EpcisConstants.TRANSACTION_EVENT)) {
265                             includeTransEvents = false;
266                         }
267                     }
268                 } else if (paramName.equals("GE_eventTime") || paramName.equals("LT_eventTime")
269                         || paramName.equals("GE_recordTime") || paramName.equals("LT_recordTime")) {
270                 	Calendar cal = parseAsCalendar(paramValue, paramName);
271                     Operation op = Operation.valueOf(paramName.substring(0, 2));
272                     String eventField = paramName.substring(3, paramName.length()) + "Ms";
273                     aggrEventQuery.addEventQueryParam(eventField, op, cal.getTimeInMillis());
274                     objEventQuery.addEventQueryParam(eventField, op, cal.getTimeInMillis());
275                     quantEventQuery.addEventQueryParam(eventField, op, cal.getTimeInMillis());
276                     transEventQuery.addEventQueryParam(eventField, op, cal.getTimeInMillis());
277 
278                 } else if (paramName.equals("EQ_action")) {
279                     // QuantityEvents have no "action" field, thus exclude them
280                     includeQuantEvents = false;
281                     ArrayOfString aos = parseAsArrayOfString(paramValue);
282                     if (!aos.getString().isEmpty()) {
283                         checkActionValues(aos.getString());
284                         aggrEventQuery.addEventQueryParam("action", Operation.EQ, aos.getString());
285                         objEventQuery.addEventQueryParam("action", Operation.EQ, aos.getString());
286                         transEventQuery.addEventQueryParam("action", Operation.EQ, aos.getString());
287                     }
288 
289                 } else if (paramName.equals("EQ_bizStep") || paramName.equals("EQ_disposition")
290                         || paramName.equals("EQ_readPoint") || paramName.equals("EQ_bizLocation")) {
291                     ArrayOfString aos = parseAsArrayOfString(paramValue);
292                     if (!aos.getString().isEmpty()) {
293                         String eventField = paramName.substring(3, paramName.length());
294                         aggrEventQuery.addEventQueryParam(eventField, Operation.EQ, aos.getString());
295                         objEventQuery.addEventQueryParam(eventField, Operation.EQ, aos.getString());
296                         quantEventQuery.addEventQueryParam(eventField, Operation.EQ, aos.getString());
297                         transEventQuery.addEventQueryParam(eventField, Operation.EQ, aos.getString());
298                     }
299 
300                 } else if (paramName.equals("WD_readPoint") || paramName.equals("WD_bizLocation")) {
301                     ArrayOfString aos = parseAsArrayOfString(paramValue);
302                     if (!aos.getString().isEmpty()) {
303                         // append a "*" to each of the parameter values - this
304                         // should implement the semantics of "With Descendant"
305                         // TODO: should???
306                         CollectionUtils.transform(aos.getString(), new StringTransformer());
307                         String eventField = paramName.substring(3, paramName.length());
308                         aggrEventQuery.addEventQueryParam(eventField, Operation.WD, aos.getString());
309                         objEventQuery.addEventQueryParam(eventField, Operation.WD, aos.getString());
310                         quantEventQuery.addEventQueryParam(eventField, Operation.WD, aos.getString());
311                         transEventQuery.addEventQueryParam(eventField, Operation.WD, aos.getString());
312                     }
313 
314                 } else if (paramName.startsWith("EQ_bizTransaction_")) {
315                     // type extracted from parameter name
316                     String bizTransType = paramName.substring(18);
317                     ArrayOfString aos = parseAsArrayOfString(paramValue);
318                     if (!aos.getString().isEmpty()) {
319                         aggrEventQuery.addEventQueryParam("bizTransList.type", Operation.EQ, bizTransType);
320                         objEventQuery.addEventQueryParam("bizTransList.type", Operation.EQ, bizTransType);
321                         quantEventQuery.addEventQueryParam("bizTransList.type", Operation.EQ, bizTransType);
322                         transEventQuery.addEventQueryParam("bizTransList.type", Operation.EQ, bizTransType);
323                         aggrEventQuery.addEventQueryParam("bizTransList.bizTrans", Operation.EQ, aos.getString());
324                         objEventQuery.addEventQueryParam("bizTransList.bizTrans", Operation.EQ, aos.getString());
325                         quantEventQuery.addEventQueryParam("bizTransList.bizTrans", Operation.EQ, aos.getString());
326                         transEventQuery.addEventQueryParam("bizTransList.bizTrans", Operation.EQ, aos.getString());
327                     }
328 
329                 } else if (paramName.equals("MATCH_epc") || paramName.equals("MATCH_anyEPC")) {
330                     // QuantityEvents have no field for EPCs, thus exclude them
331                     includeQuantEvents = false;
332                     ArrayOfString aos = parseAsArrayOfString(paramValue);
333                     if (!aos.getString().isEmpty()) {
334                         aggrEventQuery.addEventQueryParam("childEPCs", Operation.MATCH, aos.getString());
335                         objEventQuery.addEventQueryParam("epcList", Operation.MATCH, aos.getString());
336                         transEventQuery.addEventQueryParam("epcList", Operation.MATCH, aos.getString());
337                         if (paramName.equals("MATCH_anyEPC")) {
338                             // AggregationEvent and TransactionEvent need
339                             // special treatment ("parentID" field)
340                             aggrEventQuery.setIsAnyEpc(true);
341                             transEventQuery.setIsAnyEpc(true);
342                         }
343                     }
344 
345                 } else if (paramName.equals("MATCH_parentID")) {
346                     includeQuantEvents = false;
347                     includeObjEvents = false;
348                     ArrayOfString aos = parseAsArrayOfString(paramValue);
349                     if (!aos.getString().isEmpty()) {
350                         aggrEventQuery.addEventQueryParam("parentID", Operation.MATCH, aos.getString());
351                         transEventQuery.addEventQueryParam("parentID", Operation.MATCH, aos.getString());
352                     }
353 
354                 } else if (paramName.equals("MATCH_epcClass")) {
355                     includeAggrEvents = false;
356                     includeObjEvents = false;
357                     includeTransEvents = false;
358                     ArrayOfString aos = parseAsArrayOfString(paramValue);
359                     if (!aos.getString().isEmpty()) {
360                         quantEventQuery.addEventQueryParam("epcClass", Operation.MATCH, aos.getString());
361                     }
362 
363                 } else if (paramName.endsWith("_quantity")) {
364                     includeAggrEvents = false;
365                     includeObjEvents = false;
366                     includeTransEvents = false;
367                     Operation op = Operation.valueOf(paramName.substring(0, paramName.indexOf('_')));
368                     quantEventQuery.addEventQueryParam("quantity", op, parseAsInteger(paramValue));
369 
370                 } else if (paramName.startsWith("GT_") || paramName.startsWith("GE_") || paramName.startsWith("EQ_")
371                         || paramName.startsWith("LE_") || paramName.startsWith("LT_")) {
372                     // must be an event field extension
373                     String fieldname = paramName.substring(3);
374                     String[] parts = fieldname.split("#");
375                     if (parts.length != 2) {
376                         String msg = "Invalid parameter " + paramName;
377                         throw queryParameterException(msg, null);
378                     }
379                     nofEventFieldExtensions++;
380                     String eventFieldExtBase = "extension" + nofEventFieldExtensions;
381                     EventQueryParam queryParam = parseExtensionField(eventFieldExtBase, paramName, paramValue);
382                     aggrEventQuery.addEventQueryParam(queryParam);
383                     objEventQuery.addEventQueryParam(queryParam);
384                     quantEventQuery.addEventQueryParam(queryParam);
385                     transEventQuery.addEventQueryParam(queryParam);
386                     String eventFieldExt = eventFieldExtBase + ".fieldname";
387                     aggrEventQuery.addEventQueryParam(eventFieldExt, Operation.EQ, fieldname);
388                     objEventQuery.addEventQueryParam(eventFieldExt, Operation.EQ, fieldname);
389                     quantEventQuery.addEventQueryParam(eventFieldExt, Operation.EQ, fieldname);
390                     transEventQuery.addEventQueryParam(eventFieldExt, Operation.EQ, fieldname);
391 
392                 } else if (paramName.startsWith("EXISTS_")) {
393                     String fieldname = paramName.substring(7);
394                     if (fieldname.equals("childEPCs")) {
395                         includeObjEvents = false;
396                         includeQuantEvents = false;
397                         includeTransEvents = false;
398                         aggrEventQuery.addEventQueryParam("childEPCs", Operation.EXISTS, null);
399                     } else if (fieldname.equals("epcList")) {
400                         includeAggrEvents = false;
401                         includeQuantEvents = false;
402                         objEventQuery.addEventQueryParam("epcList", Operation.EXISTS, null);
403                         transEventQuery.addEventQueryParam("epcList", Operation.EXISTS, null);
404                     } else if (fieldname.equals("action")) {
405                         includeQuantEvents = false;
406                         aggrEventQuery.addEventQueryParam("action", Operation.EXISTS, null);
407                         objEventQuery.addEventQueryParam("action", Operation.EXISTS, null);
408                         transEventQuery.addEventQueryParam("action", Operation.EXISTS, null);
409                     } else if (fieldname.equals("parentID")) {
410                         includeObjEvents = false;
411                         includeQuantEvents = false;
412                         aggrEventQuery.addEventQueryParam("parentID", Operation.EXISTS, null);
413                         transEventQuery.addEventQueryParam("parentID", Operation.EXISTS, null);
414                     } else if (fieldname.equals("quantity") || fieldname.equals("epcClass")) {
415                         includeAggrEvents = false;
416                         includeObjEvents = false;
417                         includeTransEvents = false;
418                         quantEventQuery.addEventQueryParam(fieldname, Operation.EXISTS, null);
419                     } else if (fieldname.equals("eventTime") || fieldname.equals("recordTime")
420                             || fieldname.equals("eventTimeZoneOffset") || fieldname.equals("bizStep")
421                             || fieldname.equals("disposition") || fieldname.equals("readPoint")
422                             || fieldname.equals("bizLocation") || fieldname.equals("bizTransList")) {
423                         aggrEventQuery.addEventQueryParam(fieldname, Operation.EXISTS, null);
424                         objEventQuery.addEventQueryParam(fieldname, Operation.EXISTS, null);
425                         quantEventQuery.addEventQueryParam(fieldname, Operation.EXISTS, null);
426                         transEventQuery.addEventQueryParam(fieldname, Operation.EXISTS, null);
427                     } else {
428                         // lets see if we have an extension fieldname
429                         String[] parts = fieldname.split("#");
430                         if (parts.length != 2) {
431                             String msg = "Invalid parameter " + paramName;
432                             throw queryParameterException(msg, null);
433                         }
434                         nofEventFieldExtensions++;
435                         String eventFieldExt = "extension" + nofEventFieldExtensions + ".fieldname";
436                         aggrEventQuery.addEventQueryParam(eventFieldExt, Operation.EQ, fieldname);
437                         objEventQuery.addEventQueryParam(eventFieldExt, Operation.EQ, fieldname);
438                         quantEventQuery.addEventQueryParam(eventFieldExt, Operation.EQ, fieldname);
439                         transEventQuery.addEventQueryParam(eventFieldExt, Operation.EQ, fieldname);
440                     }
441 
442                 } else if (paramName.startsWith("HASATTR_")) {
443                     // restrict by attribute name
444                     String fieldname = paramName.substring(8);
445                     ArrayOfString aos = parseAsArrayOfString(paramValue);
446                     String eventField = fieldname + ".attribute";
447                     aggrEventQuery.addEventQueryParam(eventField, Operation.EQ, aos.getString());
448                     objEventQuery.addEventQueryParam(eventField, Operation.EQ, aos.getString());
449                     quantEventQuery.addEventQueryParam(eventField, Operation.EQ, aos.getString());
450                     transEventQuery.addEventQueryParam(eventField, Operation.EQ, aos.getString());
451 
452                 } else if (paramName.startsWith("EQATTR_")) {
453                     String fieldname = paramName.substring(7);
454                     String attrname = null;
455                     String[] parts = fieldname.split("_");
456                     if (parts.length > 2) {
457                         String msg = "Query parameter has invalid format: " + paramName
458                                 + ". Expected: EQATTR_fieldname_attrname";
459                         throw queryParameterException(msg, null);
460                     } else if (parts.length == 2) {
461                         fieldname = parts[0];
462                         attrname = parts[1];
463                     }
464                     // restrict by attribute name
465                     String eventField = fieldname + ".attribute";
466                     aggrEventQuery.addEventQueryParam(eventField, Operation.EQ, attrname);
467                     objEventQuery.addEventQueryParam(eventField, Operation.EQ, attrname);
468                     quantEventQuery.addEventQueryParam(eventField, Operation.EQ, attrname);
469                     transEventQuery.addEventQueryParam(eventField, Operation.EQ, attrname);
470                     // restrict by attribute value
471                     ArrayOfString aos = parseAsArrayOfString(paramValue);
472                     eventField = eventField + ".value";
473                     aggrEventQuery.addEventQueryParam(eventField, Operation.EQ, aos.getString());
474                     objEventQuery.addEventQueryParam(eventField, Operation.EQ, aos.getString());
475                     quantEventQuery.addEventQueryParam(eventField, Operation.EQ, aos.getString());
476                     transEventQuery.addEventQueryParam(eventField, Operation.EQ, aos.getString());
477 
478                 } else if (paramName.equals("orderBy")) {
479                     orderBy = parseAsString(paramValue);
480                     if (!"eventTime".equals(orderBy) && !"recordTime".equals(orderBy) && !"quantity".equals(orderBy)) {
481                         String[] parts = orderBy.split("#");
482                         if (parts.length != 2) {
483                             String msg = "orderBy must be one of eventTime, recordTime, quantity, or an extension field";
484                             throw queryParameterException(msg, null);
485                         }
486                     }
487 
488                 } else if (paramName.equals("orderDirection")) {
489                     orderDirection = OrderDirection.valueOf(parseAsString(paramValue));
490 
491                 } else if (paramName.equals("eventCountLimit")) {
492                     eventCountLimit = parseAsInteger(paramValue).intValue();
493 
494                 } else if (paramName.equals("maxEventCount")) {
495                     maxEventCount = parseAsInteger(paramValue).intValue();
496 
497                 } else {
498                     String msg = "Unknown query parameter: " + paramName;
499                     throw queryParameterException(msg, null);
500                 }
501             } catch (ClassCastException e) {
502                 String msg = "Type of value invalid for query parameter '" + paramName + "': " + paramValue;
503                 throw queryParameterException(msg, e);
504             } catch (IllegalArgumentException e) {
505                 String msg = "Unparseable value for query parameter '" + paramName + "'. " + e.getMessage();
506                 throw queryParameterException(msg, e);
507             }
508         }
509 
510         // some more user input checks
511         if (maxEventCount > -1 && eventCountLimit > -1) {
512             String msg = "Paramters 'maxEventCount' and 'eventCountLimit' are mutually exclusive";
513             throw queryParameterException(msg, null);
514         }
515         if (orderBy == null && eventCountLimit > -1) {
516             String msg = "'eventCountLimit' may only be used when 'orderBy' is specified";
517             throw queryParameterException(msg, null);
518         }
519         if (orderBy == null && orderDirection != null) {
520             String msg = "'orderDirection' may only be used when 'orderBy' is specified";
521             throw queryParameterException(msg, null);
522         }
523         if (orderBy != null) {
524             aggrEventQuery.setOrderBy(orderBy);
525             objEventQuery.setOrderBy(orderBy);
526             quantEventQuery.setOrderBy(orderBy);
527             transEventQuery.setOrderBy(orderBy);
528             if (orderDirection != null) {
529                 aggrEventQuery.setOrderDirection(orderDirection);
530                 objEventQuery.setOrderDirection(orderDirection);
531                 quantEventQuery.setOrderDirection(orderDirection);
532                 transEventQuery.setOrderDirection(orderDirection);
533             }
534         }
535         if (eventCountLimit > -1) {
536             aggrEventQuery.setLimit(eventCountLimit);
537             objEventQuery.setLimit(eventCountLimit);
538             quantEventQuery.setLimit(eventCountLimit);
539             transEventQuery.setLimit(eventCountLimit);
540         }
541         if (maxEventCount > -1) {
542             aggrEventQuery.setMaxEventCount(maxEventCount);
543             objEventQuery.setMaxEventCount(maxEventCount);
544             quantEventQuery.setMaxEventCount(maxEventCount);
545             transEventQuery.setMaxEventCount(maxEventCount);
546         }
547 
548         List<SimpleEventQueryDTO> eventQueries = new ArrayList<SimpleEventQueryDTO>(4);
549         if (includeAggrEvents) {
550             eventQueries.add(aggrEventQuery);
551         }
552         if (includeObjEvents) {
553             eventQueries.add(objEventQuery);
554         }
555         if (includeQuantEvents) {
556             eventQueries.add(quantEventQuery);
557         }
558         if (includeTransEvents) {
559             eventQueries.add(transEventQuery);
560         }
561         return eventQueries;
562     }
563 
564     /**
565      * Parses the given parameter value as an extension field. The type of the
566      * extension field value which can be integer, float, date, or list of
567      * string.
568      */
569     private EventQueryParam parseExtensionField(String eventFieldExtBase, String paramName, Object paramValue) {
570         Operation op = Operation.valueOf(paramName.substring(0, 2));
571         String eventField;
572         Object value;
573 
574         // 1. try to parse the value as int
575         try {
576             value = parseAsInteger(paramValue);
577             eventField = eventFieldExtBase + ".intValue";
578             return new EventQueryParam(eventField, op, value);
579         } catch (NumberFormatException e) {}
580 
581         // 2. try to parse the value as float
582         try {
583             value = parseAsFloat(paramValue);
584             eventField = eventFieldExtBase + ".floatValue";
585             return new EventQueryParam(eventField, op, value);
586         } catch (NumberFormatException e) {}
587 
588         // 3. try to parse the value as date
589         try {
590             value = parseAsCalendar(paramValue, paramName);
591             eventField = eventFieldExtBase + ".dateValue";
592             return new EventQueryParam(eventField, op, value);
593         } catch (QueryParameterExceptionResponse e) {}
594 
595         // 4. try to parse the value as array of string
596         try {
597             ArrayOfString aos = parseAsArrayOfString(paramValue);
598             if (!aos.getString().isEmpty()) {
599                 value = aos.getString();
600                 eventField = eventFieldExtBase + ".strValue";
601                 return new EventQueryParam(eventField, op, value);
602             }
603         } catch (Throwable t) {}
604 
605         // last effort: parse the value as string
606         value = parseAsString(paramValue);
607         eventField = eventFieldExtBase + ".strValue";
608         return new EventQueryParam(eventField, op, value);
609     }
610 
611     /**
612      * Checks if the given List contains valid event type strings, i.e.,
613      * AggregationEvent, ObjectEvent, QuantityEvent, or TransactionEvent
614      * 
615      * @param eventTypes
616      *            The List of Strings to check.
617      * @throws QueryParameterExceptionResponse
618      *             If one of the values in the given List is not one of the
619      *             valid event types.
620      */
621     private void checkEventTypes(List<String> eventTypes) throws QueryParameterExceptionResponse {
622         for (String eventType : eventTypes) {
623             if (!EpcisConstants.EVENT_TYPES.contains(eventType)) {
624                 String msg = "Unsupported eventType: " + eventType;
625                 throw queryParameterException(msg, null);
626             }
627         }
628     }
629 
630     /**
631      * Parses the given query parameter value as String.
632      * 
633      * @param queryParamValue
634      *            The query parameter value to be parsed as String.
635      * @return The Float holding the value of the query parameter.
636      */
637     private String parseAsString(Object queryParamValue) throws ClassCastException {
638         if (queryParamValue instanceof String) {
639             return (String) queryParamValue;
640         } else if (queryParamValue instanceof Element) {
641             Element elem = (Element) queryParamValue;
642             return elem.getTextContent().trim();
643         } else {
644             return queryParamValue.toString();
645         }
646     }
647 
648     /**
649      * Parses the given query parameter value as Float.
650      * 
651      * @param queryParamValue
652      *            The query parameter value to be parsed as Float.
653      * @return The Float holding the value of the query parameter.
654      * @throws NumberFormatException
655      *             If the query parameter value cannot be parsed as Float.
656      */
657     private Float parseAsFloat(Object queryParamValue) throws NumberFormatException {
658         if (queryParamValue instanceof Float) {
659             return (Float) queryParamValue;
660         } else if (queryParamValue instanceof Element) {
661             Element elem = (Element) queryParamValue;
662             return Float.valueOf(elem.getTextContent().trim());
663         } else {
664             return Float.valueOf(queryParamValue.toString());
665         }
666     }
667 
668     /**
669      * Parses the given query parameter value as Timestamp.
670      * 
671      * @param queryParamValue
672      *            The query parameter value to be parsed as Timestamp.
673      * @param queryParamName
674      *            The query parameter name.
675      * @return The Timestamp holding the value of the query parameter.
676      * @throws QueryParameterExceptionResponse
677      *             If the query parameter value cannot be parsed as Timestamp.
678      */
679     private Calendar parseAsCalendar(Object queryParamValue, String queryParamName)
680             throws QueryParameterExceptionResponse {
681         Calendar cal;
682         if (queryParamValue instanceof Calendar) {
683             // Axis returns a Calendar instance
684             cal = (Calendar) queryParamValue;
685         } else if (queryParamValue instanceof XMLGregorianCalendar) {
686             // CXF returns an XMLGregorianCalendar instance if the
687             // XML type is specified
688             cal = ((XMLGregorianCalendar) queryParamValue).toGregorianCalendar();
689         } else {
690             // try to parse the value manually
691             String date = null;
692             if (queryParamValue instanceof Element) {
693                 // CXF returns an Element instance if no XML type
694                 // was specified in the request
695                 Element elem = (Element) queryParamValue;
696                 date = elem.getTextContent().trim();
697             } else {
698                 date = queryParamValue.toString();
699             }
700             if (LOG.isDebugEnabled()) {
701                 LOG.debug("Trying to parse the value (" + date + ") for parameter " + queryParamName + " as date/time");
702             }
703             try {
704                 cal = TimeParser.parseAsCalendar(date);
705             } catch (ParseException e) {
706                 String msg = "Unable to parse the value for query parameter '" + queryParamName + "' as date/time";
707                 throw queryParameterException(msg, e);
708             }
709         }
710         return cal;
711     }
712 
713     /**
714      * Parses the given query parameter value as Integer.
715      * 
716      * @param queryParamValue
717      *            The query parameter value to be parsed as Integer.
718      * @return The Integer holding the value of the query parameter.
719      * @throws NumberFormatException
720      *             If the query parameter value cannot be parsed as Integer.
721      */
722     private Integer parseAsInteger(Object queryParamValue) throws NumberFormatException {
723         if (queryParamValue instanceof Integer) {
724             return (Integer) queryParamValue;
725         } else if (queryParamValue instanceof Element) {
726             Element elem = (Element) queryParamValue;
727             return Integer.valueOf(elem.getTextContent().trim());
728         } else {
729             return Integer.valueOf(queryParamValue.toString());
730         }
731     }
732 
733     /**
734      * Parses the given query parameter value into an ArrayOfString object.
735      * 
736      * @param queryParamValue
737      *            The value of the query parameter to be parsed.
738      * @return The ArrayOfString object representing the list of strings from
739      *         the given parameter value.
740      * @throws ClassCastException
741      *             If the given paramValue instance cannot be cast to either an
742      *             ArrayOfString or Element class.
743      * @throws QueryParameterExceptionResponse
744      *             If the given value does not correspond to valid ArrayOfString
745      *             syntax, i.e., a list of matching
746      *             <code>&lt;string&gt;</code> <code>&lt;/string&gt;</code>
747      *             tags are expected.
748      */
749     private ArrayOfString parseAsArrayOfString(Object queryParamValue) throws ClassCastException,
750             QueryParameterExceptionResponse {
751         try {
752             return (ArrayOfString) queryParamValue;
753         } catch (ClassCastException e) {
754             // trying to parse manually
755             Element elem = (Element) queryParamValue;
756             NodeList strings = elem.getChildNodes();
757             ArrayOfString aos = new ArrayOfString();
758             for (int i = 0; i < strings.getLength(); i++) {
759                 if (strings.item(i).getNodeType() == Node.ELEMENT_NODE) {
760                     if ("string".equalsIgnoreCase(strings.item(i).getNodeName())) {
761                         String s = strings.item(i).getTextContent().trim();
762                         aos.getString().add(s);
763                     } else {
764                         String msg = "Invalid ArrayOfString syntax: matching <string> </string> tags expected";
765                         throw queryParameterException(msg, null);
766                     }
767                 }
768             }
769             if (LOG.isDebugEnabled()) {
770                 LOG.debug("ArrayOfString parsed to: " + aos.getString());
771             }
772             return aos;
773         }
774     }
775 
776     /**
777      * Runs a MasterDataQuery for the given QueryParam array and returns the
778      * QueryResults.
779      * 
780      * @param queryParams
781      *            The parameters for running the MasterDataQuery.
782      * @return The QueryResults.
783      * @throws SQLException
784      *             If an error accessing the database occurred.
785      * @throws QueryParameterException
786      *             If one of the provided QueryParam is invalid.
787      * @throws ImplementationException
788      *             If a service implementation error occurred.
789      * @throws QueryTooLargeException
790      *             If the query is too large to be executed.
791      */
792     private MasterDataQueryDTO constructMasterDataQuery(final QueryParams queryParams)
793             throws QueryParameterExceptionResponse {
794         MasterDataQueryDTO mdQuery = new MasterDataQueryDTO();
795 
796         // a sorted List of query parameter names - keeps track of the processed
797         // names in order to cope with duplicates
798         List<String> sortedParamNames = new ArrayList<String>();
799 
800         Boolean includeAttributes = null;
801         Boolean includeChildren = null;
802         List<String> vocabularyTypes = null;
803         List<String> includedAttributeNames = null;
804 
805         for (QueryParam param : queryParams.getParam()) {
806             String paramName = param.getName();
807             Object paramValue = param.getValue();
808 
809             // check for null value
810             if (paramName == null || "".equals(paramName)) {
811                 String msg = "Missing name for a query parameter";
812                 throw queryParameterException(msg, null);
813             }
814             if (paramValue == null) {
815                 String msg = "Missing value for query parameter '" + paramName + "'";
816                 throw queryParameterException(msg, null);
817             }
818             // check if the current query parameter has already been provided
819             int index = Collections.binarySearch(sortedParamNames, paramName);
820             if (index < 0) {
821                 // we have not yet seen this query parameter name - ok
822                 sortedParamNames.add(-index - 1, paramName);
823             } else {
824                 // we have already handled this query parameter name - not ok
825                 String msg = "Query parameter '" + paramName + "' provided more than once";
826                 throw queryParameterException(msg, null);
827             }
828 
829             try {
830                 if (paramName.equals("includeAttributes")) {
831                     // defaults to 'false' if an invalid value is provided!
832                     includeAttributes = Boolean.valueOf(parseAsString(paramValue));
833                     mdQuery.setIncludeAttributes(includeAttributes.booleanValue());
834 
835                 } else if (paramName.equals("includeChildren")) {
836                     // defaults to 'false' if an invalid value is provided!
837                     includeChildren = Boolean.valueOf(parseAsString(paramValue));
838                     mdQuery.setIncludeChildren(includeChildren.booleanValue());
839 
840                 } else if (paramName.equals("maxElementCount")) {
841                     int maxElementCount = parseAsInteger(paramValue).intValue();
842                     mdQuery.setMaxElementCount(maxElementCount);
843 
844                 } else if (paramName.equals("vocabularyName")) {
845                     ArrayOfString aos = parseAsArrayOfString(paramValue);
846                     vocabularyTypes = aos.getString();
847 
848                 } else if (paramName.equals("attributeNames")) {
849                     ArrayOfString aos = parseAsArrayOfString(paramValue);
850                     includedAttributeNames = aos.getString();
851 
852                 } else if (paramName.equals("EQ_name")) {
853                     ArrayOfString aos = parseAsArrayOfString(paramValue);
854                     mdQuery.setVocabularyEqNames(aos.getString());
855 
856                 } else if (paramName.equals("WD_name")) {
857                     ArrayOfString aos = parseAsArrayOfString(paramValue);
858                     mdQuery.setVocabularyWdNames(aos.getString());
859 
860                 } else if (paramName.equals("HASATTR")) {
861                     ArrayOfString aos = parseAsArrayOfString(paramValue);
862                     mdQuery.setAttributeNames(aos.getString());
863 
864                 } else if (paramName.startsWith("EQATTR_")) {
865                     String attrName = paramName.substring(7);
866                     ArrayOfString aos = parseAsArrayOfString(paramValue);
867                     mdQuery.addAttributeNameAndValues(attrName, aos.getString());
868 
869                 }
870             } catch (ClassCastException e) {
871                 String msg = "The type of the value for query parameter '" + paramName + "': " + paramValue
872                         + " is invalid";
873                 throw queryParameterException(msg, e);
874             }
875         }
876 
877         // check for missing parameters
878         if (includeAttributes == null || includeChildren == null) {
879             String missing = (includeAttributes == null) ? " includeAttributes" : "";
880             missing += (includeChildren == null) ? " includeChildren" : "";
881             String msg = "Missing required masterdata query parameter(s):" + missing;
882             throw queryParameterException(msg, null);
883         }
884         if (includeAttributes.booleanValue() && includedAttributeNames != null) {
885             mdQuery.setIncludedAttributeNames(includedAttributeNames);
886         }
887 
888         if (vocabularyTypes == null) {
889             // include all vocabularies
890             vocabularyTypes = EpcisConstants.VOCABULARY_TYPES;
891         }
892         mdQuery.setVocabularyTypes(vocabularyTypes);
893 
894         return mdQuery;
895     }
896 
897     /**
898      * Writes the given message and exception to the application's log file,
899      * creates a QueryParameterException from the given message, and returns a
900      * new QueryParameterExceptionResponse. Use this method to conveniently
901      * return a user error message back to the requesting service caller, e.g.:
902      * 
903      * <pre>
904      * String msg = &quot;unable to parse query parameter&quot;
905      * throw new queryParameterException(msg, null);
906      * </pre>
907      * 
908      * @param msg
909      *            A user error message.
910      * @param e
911      *            An internal exception - this exception will not be delivered
912      *            back to the service caller as it contains application specific
913      *            information. It will be used to print some details about the
914      *            user error to the log file (useful for debugging).
915      * @return A new QueryParameterExceptionResponse containing the given user
916      *         error message.
917      */
918     private QueryParameterExceptionResponse queryParameterException(String msg, Exception e) {
919         LOG.info("QueryParameterException: " + msg);
920         if (LOG.isTraceEnabled() && e != null) {
921             LOG.trace("Exception details: " + e.getMessage(), e);
922         }
923         QueryParameterException qpe = new QueryParameterException();
924         qpe.setReason(msg);
925         return new QueryParameterExceptionResponse(msg, qpe);
926     }
927 
928     /**
929      * Checks if the given action values are valid, i.e. all values must be one
930      * of ADD, OBSERVE, or DELETE. Throws an exception if one of the values is
931      * invalid.
932      * 
933      * @param actions
934      *            The action values to be checked.
935      * @throws QueryParameterException
936      *             If one of the action values are invalid.
937      */
938     private void checkActionValues(final List<String> actions) throws QueryParameterExceptionResponse {
939         for (String action : actions) {
940             if (!(action.equalsIgnoreCase("ADD") || action.equalsIgnoreCase("OBSERVE") || action.equalsIgnoreCase("DELETE"))) {
941                 String msg = "Invalid value for parameter EQ_action: " + action
942                         + " - must be one of ADD, OBSERVE, or DELETE";
943                 throw queryParameterException(msg, null);
944             }
945         }
946     }
947 
948     /**
949      * Saves the map with the subscriptions to the message context.
950      * 
951      * @param subscriptions
952      *            The map with the subscriptions.
953      */
954     private void saveSubscriptions(final Map<String, QuerySubscriptionScheduled> subscriptions) {
955         servletContext.setAttribute("subscribedMap", subscriptions);
956     }
957 
958     /**
959      * Retrieves the map with the subscriptions from the servlet context.
960      * 
961      * @return The map with the subscriptions.
962      * @throws ImplementationException
963      *             If the map could not be reloaded.
964      * @throws SQLException
965      *             If a database error occurred.
966      */
967     @SuppressWarnings("unchecked")
968     private Map<String, QuerySubscriptionScheduled> loadSubscriptions(QueryOperationsSession session)
969             throws ImplementationExceptionResponse, SQLException {
970         LOG.debug("Retrieving subscriptions from application context");
971         Object subscribedMap = servletContext.getAttribute("subscribedMap");
972         Map<String, QuerySubscriptionScheduled> subscriptions = (HashMap<String, QuerySubscriptionScheduled>) subscribedMap;
973         if (subscriptions == null) {
974             LOG.debug("Subscriptions not found - retrieving subscriptions from database");
975             subscriptions = backend.fetchSubscriptions(session);
976         }
977         return subscriptions;
978     }
979 
980     /**
981      * {@inheritDoc}
982      */
983     public List<String> getQueryNames() throws SecurityExceptionResponse, ValidationExceptionResponse,
984             ImplementationExceptionResponse {
985         LOG.info("Invoking 'getQueryNames'");
986         return QUERYNAMES;
987     }
988 
989     /**
990      * {@inheritDoc}
991      */
992     public String getStandardVersion() throws SecurityExceptionResponse, ValidationExceptionResponse,
993             ImplementationExceptionResponse {
994         LOG.info("Invoking 'getStandardVersion'");
995         return STD_VERSION;
996     }
997 
998     /**
999      * {@inheritDoc}
1000      */
1001     public List<String> getSubscriptionIDs(String queryName) throws NoSuchNameExceptionResponse,
1002             SecurityExceptionResponse, ValidationExceptionResponse, ImplementationExceptionResponse {
1003         try {
1004             LOG.info("Invoking 'getSubscriptionIDs'");
1005             QueryOperationsSession session = null;
1006             try {
1007                 session = backend.openSession(dataSource);
1008 
1009                 // TODO: filter by queryName?!
1010                 Map<String, QuerySubscriptionScheduled> subscribedMap = loadSubscriptions(session);
1011                 Set<String> temp = subscribedMap.keySet();
1012                 return new ArrayList<String>(temp);
1013             } finally {
1014                 if (session != null) {
1015                     session.close();
1016                 }
1017                 LOG.debug("DB connection closed");
1018             }
1019         } catch (SQLException e) {
1020             ImplementationException iex = new ImplementationException();
1021             String msg = "SQL error during query execution: " + e.getMessage();
1022             LOG.error(msg, e);
1023             iex.setReason(msg);
1024             iex.setSeverity(ImplementationExceptionSeverity.ERROR);
1025             throw new ImplementationExceptionResponse(msg, iex, e);
1026         }
1027     }
1028 
1029     /**
1030      * {@inheritDoc}
1031      */
1032     public String getVendorVersion() throws SecurityExceptionResponse, ValidationExceptionResponse,
1033             ImplementationExceptionResponse {
1034         LOG.info("Invoking 'getVendorVersion'");
1035         return serviceVersion;
1036     }
1037 
1038     /**
1039      * {@inheritDoc}
1040      */
1041     public QueryResults poll(String queryName, QueryParams queryParams) throws NoSuchNameExceptionResponse,
1042             QueryParameterExceptionResponse, QueryTooComplexExceptionResponse, QueryTooLargeExceptionResponse,
1043             SecurityExceptionResponse, ValidationExceptionResponse, ImplementationExceptionResponse {
1044         try {
1045             LOG.info("Invoking 'poll'");
1046             QueryOperationsSession session = null;
1047             try {
1048                 session = backend.openSession(dataSource);
1049                 QueryResultsBody resultsBody = null;
1050                 if (queryName.equals("SimpleEventQuery")) {
1051                     LOG.info("This is a SimpleEventQuery");
1052                     EventListType eventList = new EventListType();
1053                     List<SimpleEventQueryDTO> eventQueries = constructSimpleEventQueries(queryParams);
1054                     // run queries sequentially
1055                     // TODO: might want to run them in parallel!
1056                     String orderBy = null;
1057                     OrderDirection orderDirection = null;
1058                     int limit = -1;
1059                     for (SimpleEventQueryDTO eventQuery : eventQueries) {
1060                         if (eventQuery.getOrderBy() != null) {
1061                             orderBy = eventQuery.getOrderBy();
1062                             orderDirection = eventQuery.getOrderDirection();
1063                             limit = eventQuery.getLimit();
1064                         }
1065                         backend.runSimpleEventQuery(session, eventQuery,
1066                                 eventList.getObjectEventOrAggregationEventOrQuantityEvent());
1067                     }
1068                     eventList = checkOrdering(eventList, orderBy, orderDirection, limit);
1069 
1070                     resultsBody = new QueryResultsBody();
1071                     resultsBody.setEventList(eventList);
1072                 } else if (queryName.equals("SimpleMasterDataQuery")) {
1073                     LOG.info("This is a SimpleMasterDataQuery");
1074                     VocabularyListType vocList = new VocabularyListType();
1075                     MasterDataQueryDTO mdQuery = constructMasterDataQuery(queryParams);
1076                     backend.runMasterDataQuery(session, mdQuery, vocList.getVocabulary());
1077 
1078                     resultsBody = new QueryResultsBody();
1079                     resultsBody.setVocabularyList(vocList);
1080                 } else {
1081                     session.close();
1082                     String msg = "Unsupported query name '" + queryName + "' provided";
1083                     LOG.info("NoSuchNameException: " + msg);
1084                     NoSuchNameException e = new NoSuchNameException();
1085                     e.setReason(msg);
1086                     throw new NoSuchNameExceptionResponse(msg, e);
1087                 }
1088                 QueryResults results = new QueryResults();
1089                 results.setResultsBody(resultsBody);
1090                 results.setQueryName(queryName);
1091 
1092                 LOG.info("poll request for '" + queryName + "' succeeded");
1093                 return results;
1094             } finally {
1095                 if (session != null) {
1096                     session.close();
1097                 }
1098                 LOG.debug("DB connection closed");
1099             }
1100         } catch (SQLException e) {
1101             ImplementationException iex = new ImplementationException();
1102             String msg = "SQL error during query execution: " + e.getMessage();
1103             LOG.error(msg, e);
1104             iex.setReason(msg);
1105             iex.setSeverity(ImplementationExceptionSeverity.ERROR);
1106             throw new ImplementationExceptionResponse(msg, iex, e);
1107         }
1108     }
1109 
1110     /**
1111      * @param eventList
1112      * @param limit
1113      * @param orderDirection
1114      * @param orderBy
1115      * @return
1116      */
1117     private EventListType checkOrdering(EventListType eventList, String orderBy, OrderDirection orderDirection,
1118             int limit) {
1119         if (orderBy == null) {
1120             // no ordering specified
1121             return eventList;
1122         }
1123         if ("quantity".equals(orderBy)) {
1124             // order by quantity can only return QuantityEvents
1125             return eventList;
1126         }
1127         int size = eventList.getObjectEventOrAggregationEventOrQuantityEvent().size();
1128         if (limit > -1 && size == limit) {
1129             // there was only a single event type to be ordered - this has been
1130             // taken care of appropriately by the previous query
1131             return eventList;
1132         }
1133         LOG.debug("Need to apply sorting across the different event types (sortBy=" + orderBy + ")");
1134         boolean orderByEventTime = "eventTime".equals(orderBy);
1135         Comparator<Object> comparator = new EventComparator(orderByEventTime, orderDirection);
1136         Collections.sort(eventList.getObjectEventOrAggregationEventOrQuantityEvent(), comparator);
1137         if (limit > -1 && size > limit) {
1138             LOG.debug("Need to apply global limit to events (limit=" + limit + ")");
1139             // clear everything beyond limit
1140             eventList.getObjectEventOrAggregationEventOrQuantityEvent().subList(limit, size).clear();
1141         }
1142         return eventList;
1143     }
1144 
1145     /**
1146      * {@inheritDoc}
1147      */
1148     public void subscribe(String queryName, QueryParams params, String dest, SubscriptionControls controls,
1149             String subscriptionID) throws NoSuchNameExceptionResponse, InvalidURIExceptionResponse,
1150             DuplicateSubscriptionExceptionResponse, QueryParameterExceptionResponse, QueryTooComplexExceptionResponse,
1151             SubscriptionControlsExceptionResponse, SubscribeNotPermittedExceptionResponse, SecurityExceptionResponse,
1152             ValidationExceptionResponse, ImplementationExceptionResponse {
1153         try {
1154             LOG.info("Invoking 'subscribe'");
1155             QueryOperationsSession session = null;
1156             try {
1157                 session = backend.openSession(dataSource);
1158                 String triggerURI = controls.getTrigger();
1159                 QuerySubscriptionScheduled newSubscription = null;
1160                 Schedule schedule = null;
1161                 Calendar initialRecordTime;
1162                 try {
1163                 	initialRecordTime = controls.getInitialRecordTime().toGregorianCalendar();
1164                 }catch(Exception e){
1165                 	initialRecordTime = GregorianCalendar.getInstance();
1166                 }
1167 
1168                 // a few input sanity checks
1169 
1170                 // dest may be null or empty. But we don't support pre-arranged
1171                 // destinations and throw an InvalidURIException according to
1172                 // the standard.
1173                 if (dest == null || dest.toString().equals("")) {
1174                     String msg = "Destination URI is empty. This implementation doesn't support pre-arranged destinations.";
1175                     LOG.info("QueryParameterException: " + msg);
1176                     InvalidURIException e = new InvalidURIException();
1177                     e.setReason(msg);
1178                     throw new InvalidURIExceptionResponse(msg, e);
1179                 }
1180                 try {
1181                     new URL(dest.toString());
1182                 } catch (MalformedURLException ex) {
1183                     String msg = "Destination URI is invalid: " + ex.getMessage();
1184                     LOG.info("InvalidURIException: " + msg);
1185                     InvalidURIException e = new InvalidURIException();
1186                     e.setReason(msg);
1187                     throw new InvalidURIExceptionResponse(msg, e, ex);
1188                 }
1189 
1190                 // check query name
1191                 if (!QUERYNAMES.contains(queryName)) {
1192                     String msg = "Illegal query name '" + queryName + "'";
1193                     LOG.info("NoSuchNameException: " + msg);
1194                     NoSuchNameException e = new NoSuchNameException();
1195                     e.setReason(msg);
1196                     throw new NoSuchNameExceptionResponse(msg, e);
1197                 }
1198 
1199                 // SimpleMasterDataQuery only valid for polling
1200                 if (queryName.equals("SimpleMasterDataQuery")) {
1201                     String msg = "Subscription not allowed for SimpleMasterDataQuery";
1202                     LOG.info("SubscribeNotPermittedException: " + msg);
1203                     SubscribeNotPermittedException e = new SubscribeNotPermittedException();
1204                     e.setReason(msg);
1205                     throw new SubscribeNotPermittedExceptionResponse(msg, e);
1206                 }
1207 
1208                 // subscriptionID cannot be empty
1209                 if (subscriptionID == null || subscriptionID.equals("")) {
1210                     String msg = "SubscriptionID is empty. Choose a valid subscriptionID";
1211                     LOG.info(msg);
1212                     ValidationException e = new ValidationException();
1213                     e.setReason(msg);
1214                     throw new ValidationExceptionResponse(msg, e);
1215                 }
1216 
1217                 // check for already existing subscriptionID
1218                 if (backend.fetchExistsSubscriptionId(session, subscriptionID)) {
1219                     String msg = "SubscriptionID '" + subscriptionID
1220                             + "' already exists. Choose a different subscriptionID";
1221                     LOG.info("DuplicateSubscriptionException: " + msg);
1222                     DuplicateSubscriptionException e = new DuplicateSubscriptionException();
1223                     e.setReason(msg);
1224                     throw new DuplicateSubscriptionExceptionResponse(msg, e);
1225                 }
1226 
1227                 // trigger and schedule may no be used together, but one of them
1228                 // must be set
1229                 if (controls.getSchedule() != null && controls.getTrigger() != null) {
1230                     String msg = "Schedule and trigger cannot be used together";
1231                     LOG.info("SubscriptionControlsException: " + msg);
1232                     SubscriptionControlsException e = new SubscriptionControlsException();
1233                     e.setReason(msg);
1234                     throw new SubscriptionControlsExceptionResponse(msg, e);
1235                 }
1236                 if (controls.getSchedule() == null && controls.getTrigger() == null) {
1237                     String msg = "Either schedule or trigger has to be provided";
1238                     LOG.info("SubscriptionControlsException: " + msg);
1239                     SubscriptionControlsException e = new SubscriptionControlsException();
1240                     e.setReason(msg);
1241                     throw new SubscriptionControlsExceptionResponse(msg, e);
1242                 }
1243                 if (controls.getSchedule() != null) {
1244                     LOG.debug("Received new scheduled query.");
1245                     // Scheduled Query -> parse schedule
1246                     schedule = new Schedule(controls.getSchedule());
1247                     newSubscription = new QuerySubscriptionScheduled(subscriptionID, params, dest,
1248                             Boolean.valueOf(controls.isReportIfEmpty()), initialRecordTime, initialRecordTime,
1249                             schedule, queryName);
1250                 } else {
1251                     LOG.debug("Received new triggered query.");
1252                     // need to set schedule which says how often the trigger
1253                     // condition is checked.
1254                     QuerySchedule qSchedule = new QuerySchedule();
1255                     qSchedule.setSecond(triggerConditionSeconds);
1256                     if (triggerConditionMinutes != null) {
1257                         qSchedule.setMinute(triggerConditionMinutes);
1258                     }
1259                     schedule = new Schedule(qSchedule);
1260                     QuerySubscriptionTriggered trigger = new QuerySubscriptionTriggered(subscriptionID, params, dest,
1261                             Boolean.valueOf(controls.isReportIfEmpty()), initialRecordTime, initialRecordTime,
1262                             queryName, triggerURI, schedule);
1263                     newSubscription = trigger;
1264                 }
1265 
1266                 // load subscriptions
1267                 Map<String, QuerySubscriptionScheduled> subscribedMap = loadSubscriptions(session);
1268 
1269                 // store the Query to the database, the local hash map, and the
1270                 // application context
1271                 backend.storeSupscriptions(session, params, dest, subscriptionID, controls, triggerURI,
1272                         newSubscription, queryName, schedule);
1273                 subscribedMap.put(subscriptionID, newSubscription);
1274                 saveSubscriptions(subscribedMap);
1275             } finally {
1276                 if (session != null) {
1277                     session.close();
1278                 }
1279                 LOG.debug("DB connection closed");
1280             }
1281         } catch (SQLException e) {
1282             String msg = "SQL error during query execution: " + e.getMessage();
1283             LOG.error(msg, e);
1284             ImplementationException iex = new ImplementationException();
1285             iex.setReason(msg);
1286             iex.setSeverity(ImplementationExceptionSeverity.ERROR);
1287             throw new ImplementationExceptionResponse(msg, iex, e);
1288         }
1289     }
1290 
1291     /**
1292      * {@inheritDoc}
1293      */
1294     public void unsubscribe(String subscriptionID) throws NoSuchSubscriptionExceptionResponse,
1295             SecurityExceptionResponse, ValidationExceptionResponse, ImplementationExceptionResponse {
1296         try {
1297             LOG.info("Invoking 'unsubscribe'");
1298             QueryOperationsSession session = null;
1299             try {
1300                 session = backend.openSession(dataSource);
1301                 Map<String, QuerySubscriptionScheduled> subscribedMap = loadSubscriptions(session);
1302                 if (subscribedMap.containsKey(subscriptionID)) {
1303                     // remove subscription from local hash map
1304                     QuerySubscriptionScheduled toDelete = subscribedMap.get(subscriptionID);
1305                     toDelete.stopSubscription();
1306                     subscribedMap.remove(subscriptionID);
1307                     saveSubscriptions(subscribedMap);
1308 
1309                     // delete subscription from database
1310                     backend.deleteSubscription(session, subscriptionID);
1311                 } else {
1312                     String msg = "There is no subscription with ID '" + subscriptionID + "'";
1313                     LOG.info("NoSuchSubscriptionException: " + msg);
1314                     NoSuchSubscriptionException e = new NoSuchSubscriptionException();
1315                     e.setReason(msg);
1316                     throw new NoSuchSubscriptionExceptionResponse(msg, e);
1317                 }
1318             } finally {
1319                 if (session != null) {
1320                     session.close();
1321                 }
1322                 LOG.debug("DB connection closed");
1323             }
1324         } catch (SQLException e) {
1325             ImplementationException iex = new ImplementationException();
1326             String msg = "SQL error during query execution: " + e.getMessage();
1327             LOG.error(msg, e);
1328             iex.setReason(msg);
1329             iex.setSeverity(ImplementationExceptionSeverity.ERROR);
1330             throw new ImplementationExceptionResponse(msg, iex, e);
1331         }
1332     }
1333 
1334     /**
1335      * A Transformer which expects a String instance, appends a "*" to the end
1336      * of the String, and returns the new String.
1337      * 
1338      * @author Marco Steybe
1339      */
1340     private static class StringTransformer implements Transformer {
1341         public Object transform(Object o) {
1342             if (o instanceof String) {
1343                 o = ((String) o).concat("*");
1344             }
1345             return o;
1346         }
1347     }
1348 
1349     /**
1350      * @return the dataSource
1351      */
1352     public DataSource getDataSource() {
1353         return dataSource;
1354     }
1355 
1356     /**
1357      * @param dataSource
1358      *            the dataSource to set
1359      */
1360     public void setDataSource(DataSource dataSource) {
1361         this.dataSource = dataSource;
1362     }
1363 
1364     /**
1365      * @return the maxQueryRows
1366      */
1367     public int getMaxQueryRows() {
1368         return maxQueryRows;
1369     }
1370 
1371     /**
1372      * @param maxQueryRows
1373      *            the maxQueryRows to set
1374      */
1375     public void setMaxQueryRows(int maxQueryRows) {
1376         this.maxQueryRows = maxQueryRows;
1377     }
1378 
1379     /**
1380      * @return the maxQueryTime
1381      */
1382     public int getMaxQueryTime() {
1383         return maxQueryTime;
1384     }
1385 
1386     /**
1387      * @param maxQueryTime
1388      *            the maxQueryTime to set
1389      */
1390     public void setMaxQueryTime(int maxQueryTime) {
1391         this.maxQueryTime = maxQueryTime;
1392     }
1393 
1394     /**
1395      * @return the triggerConditionSeconds
1396      */
1397     public String getTriggerConditionSeconds() {
1398         return triggerConditionSeconds;
1399     }
1400 
1401     /**
1402      * @param triggerConditionSeconds
1403      *            the triggerConditionSeconds to set
1404      */
1405     public void setTriggerConditionSeconds(String triggerConditionSeconds) {
1406         this.triggerConditionSeconds = triggerConditionSeconds;
1407     }
1408 
1409     /**
1410      * @return the triggerConditionMinutes
1411      */
1412     public String getTriggerConditionMinutes() {
1413         return triggerConditionMinutes;
1414     }
1415 
1416     /**
1417      * @param triggerConditionMinutes
1418      *            the triggerConditionMinutes to set
1419      */
1420     public void setTriggerConditionMinutes(String triggerConditionMinutes) {
1421         this.triggerConditionMinutes = triggerConditionMinutes;
1422     }
1423 
1424     /**
1425      * @param servletContext
1426      *            the servletContextservletContext to set
1427      */
1428     public void setServletContext(ServletContext servletContext) {
1429         this.servletContext = servletContext;
1430     }
1431 
1432     /**
1433      * @return the serviceVersion
1434      */
1435     public String getServiceVersion() {
1436         return serviceVersion;
1437     }
1438 
1439     /**
1440      * @param serviceVersion
1441      *            the serviceVersion to set
1442      */
1443     public void setServiceVersion(String serviceVersion) {
1444         if (!"".equals(serviceVersion)) {
1445             // serviceVersion must be a valid URL
1446             try {
1447                 new URL(serviceVersion);
1448             } catch (MalformedURLException e) {
1449                 serviceVersion = "http://www.fosstrak.org/epcis/" + serviceVersion;
1450             }
1451         }
1452         this.serviceVersion = serviceVersion;
1453     }
1454 
1455     /**
1456      * @return the backend
1457      */
1458     public QueryOperationsBackend getBackend() {
1459         return backend;
1460     }
1461 
1462     /**
1463      * @param backend
1464      *            the backend to set
1465      */
1466     public void setBackend(QueryOperationsBackend backend) {
1467         this.backend = backend;
1468     }
1469 
1470     /**
1471      * Compares two EPCIS events according to their eventTime or recordTime.
1472      * Careful: the objects to be compared are instances of EPCISEvent,
1473      * otherwise a ClassCastException will be thrown.
1474      * 
1475      * @author Marco Steybe
1476      */
1477     public class EventComparator implements Comparator<Object> {
1478         private boolean orderByEventTime = false;
1479         private OrderDirection orderDirection = null;
1480 
1481         public EventComparator(boolean orderByEventTime, OrderDirection orderDirection) {
1482             this.orderByEventTime = orderByEventTime;
1483             this.orderDirection = orderDirection;
1484         }
1485 
1486         public int compare(Object o1, Object o2) {
1487             EPCISEventType event1 = (EPCISEventType) o1;
1488             EPCISEventType event2 = (EPCISEventType) o2;
1489             if (orderByEventTime) {
1490                 if (orderDirection == OrderDirection.ASC) {
1491                     return event1.getEventTime().compare(event2.getEventTime());
1492                 } else {
1493                     return event2.getEventTime().compare(event1.getEventTime());
1494                 }
1495             } else {
1496                 // order by recordTime
1497                 if (orderDirection == OrderDirection.ASC) {
1498                     return event1.getRecordTime().compare(event2.getRecordTime());
1499                 } else {
1500                     return event2.getEventTime().compare(event1.getEventTime());
1501                 }
1502             }
1503         }
1504     }
1505 }