/** * Copyright (C) 2010 Orbeon, Inc. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation; either version * 2.1 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html */ package org.orbeon.oxf.processor.sql.interpreters; import org.orbeon.dom.Document; import org.orbeon.dom.Element; import org.orbeon.dom.Node; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.common.ValidationException; import org.orbeon.oxf.processor.sql.SQLFunctionLibrary; import org.orbeon.oxf.processor.sql.SQLProcessor; import org.orbeon.oxf.processor.sql.SQLProcessorInterpreterContext; import org.orbeon.oxf.util.Base64XMLReceiver; import org.orbeon.oxf.util.DateUtils; import org.orbeon.oxf.util.NetUtils; import org.orbeon.oxf.xml.XMLConstants; import org.orbeon.oxf.xml.XPathUtils; import org.orbeon.oxf.xml.XPathXMLReceiver; import org.orbeon.oxf.xml.dom4j.Dom4jUtils; import org.orbeon.oxf.xml.dom4j.LocationData; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import java.io.OutputStream; import java.math.BigDecimal; import java.sql.*; import java.util.*; public class QueryInterpreter extends SQLProcessor.InterpreterContentHandler { // private static final String SQL_TYPE_VARCHAR = "varchar"; private static final String SQL_TYPE_CLOB = "clob"; private static final String SQL_TYPE_BLOB = "blob"; private static final String SQL_TYPE_XMLTYPE = "xmltype"; public static final int QUERY = 0; public static final int UPDATE = 1; public static final int CALL = 2; private int type; private StringBuilder query; private List queryParameters; private boolean hasReplaceOrSeparator; private Iterator nodeIterator; private String debugString; public QueryInterpreter(SQLProcessorInterpreterContext interpreterContext, int type) { super(interpreterContext, false); this.type = type; } public void characters(char[] chars, int start, int length) throws SAXException { if (query == null) query = new StringBuilder(); query.append(chars, start, length); } public void startElement(String uri, String localname, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localname, qName, attributes); if (SQLProcessor.SQL_NAMESPACE_URI.equals(uri)) { if (localname.equals("param") || localname.equals("parameter")) { if (query == null) query = new StringBuilder(); // Add parameter information String direction = attributes.getValue("direction"); String type = attributes.getValue("type"); String sqlType = attributes.getValue("sql-type"); String select = attributes.getValue("select"); String separator = attributes.getValue("separator"); boolean replace = Boolean.valueOf(attributes.getValue("replace")).booleanValue(); String nullIf; { nullIf = attributes.getValue("null"); if (nullIf == null) nullIf = attributes.getValue("null-if");// legacy attribute name } if (replace || separator != null) { // Remember that we have to replace at least once hasReplaceOrSeparator = true; } else { // Add question mark for prepared statement query.append(" ? "); } // Remember parameter if (queryParameters == null) queryParameters = new ArrayList(); queryParameters.add(new QueryParameter(direction, type, sqlType, select, separator, replace, nullIf, query.length(), new LocationData(getDocumentLocator()))); } else { // This must be either a get-column or a (deprecated) simple getter final boolean isGetColumn = "get-column".equals(localname) || "get-column-value".equals(localname); final String levelString = attributes.getValue("ancestor"); final String columnName = (attributes.getValue("column-name") != null) ? attributes.getValue("column-name") : attributes.getValue("column"); // Level defaults to 1 in query int level = (levelString == null) ? 1 : Integer.parseInt(levelString); if (level < 1) throw new ValidationException("Attribute level must be 1 or greater in query", new LocationData(getDocumentLocator())); // Set value try { final ResultSet rs = getInterpreterContext().getResultSet(level); final int columnIndex = rs.findColumn(columnName); final ResultSetMetaData metadata = rs.getMetaData(); final int columnType = metadata.getColumnType(rs.findColumn(columnName)); final Object value; if (isGetColumn) { // Generic getter final String xmlType = GetterInterpreter.getXMLTypeFromAttributeStringHandleDefault(getDocumentLocator(), getInterpreterContext().getPropertySet(), attributes.getValue("type"), getInterpreterContext().getPrefixesMap(), columnType); if (Dom4jUtils.qNameToExplodedQName(XMLConstants.OPS_XMLFRAGMENT_QNAME).equals(xmlType)) { if (columnType == Types.CLOB) { value = rs.getClob(columnName); } else if (columnType == Types.BLOB) { throw new ValidationException("Cannot read a Blob as an xmlFragment type", new LocationData(getDocumentLocator())); } else { value = rs.getString(columnName); } } else { value = GetterInterpreter.getColumnValue(rs, getDocumentLocator(), columnIndex, xmlType); } } else { // Deprecated: simple getter value = GetterInterpreter.getColumnValue(rs, getDocumentLocator(), columnIndex, GetterInterpreter.getXMLTypeFromLegacyGetterName(localname)); } ((QueryParameter) queryParameters.get(queryParameters.size() - 1)).setValue(value); } catch (Exception e) { throw new ValidationException(e, new LocationData(getDocumentLocator())); } } } } public void start(String uri, String localname, String qName, Attributes attributes) throws SAXException { // Get select attribute String selectString = attributes.getValue("select"); if (selectString != null) { if (type != UPDATE) throw new ValidationException("select attribute is valid only on update element", new LocationData(getDocumentLocator())); nodeIterator = XPathUtils.selectNodeIterator( getInterpreterContext().getCurrentNode(), selectString, getInterpreterContext().getPrefixesMap(), SQLFunctionLibrary.instance(), getInterpreterContext().getFunctionContextOrNull() ); } // Get debug attribute debugString = attributes.getValue("debug"); } public void end(String uri, String localname, String qName) throws SAXException { // Validate query if (query == null) throw new ValidationException("Missing query", new LocationData(getDocumentLocator())); // Execute query try { // Create a single PreparedStatement if the query is not modified at each iteration PreparedStatement stmt = null; if (!hasReplaceOrSeparator) { final String queryString = query.toString(); if (type != CALL) { // TODO: see how we can support this: Statement.RETURN_GENERATED_KEYS stmt = getInterpreterContext().getConnection().prepareStatement(queryString); } else stmt = getInterpreterContext().getConnection().prepareCall(queryString); getInterpreterContext().setStatementString(queryString); } getInterpreterContext().setStatement(stmt); int nodeCount = 1; // Iterate through all source nodes (only one if "select" attribute is missing) for (Iterator j = (nodeIterator != null) ? nodeIterator : Collections.singletonList(getInterpreterContext().getCurrentNode()).iterator(); j.hasNext(); nodeCount++) { final Node currentNode = (Node) j.next(); // LocationData locationData = (currentNode instanceof Element) // ? (LocationData) ((Element) currentNode).getData() : null; Map prefixesMap = getInterpreterContext().getPrefixesMap(); final scala.Function2<String, Object, String> getColumnFunction = new SQLFunctionLibrary.Function2Base<String, Object, String>() { @Override public String apply(String colName, Object levelString) { final int level = (Integer) levelString; if (level < 1) throw new OXFException("Attribute level must be 1 or greater in query"); final ResultSet rs = getInterpreterContext().getResultSet(level); try { return rs.getString(colName); } catch (SQLException e) { throw new OXFException(e); } } }; getInterpreterContext().pushFunctionContext(new SQLFunctionLibrary.SQLFunctionContext(currentNode, nodeCount, getColumnFunction)); try { // Replace inline parameters StringBuilder replacedQuery = query; if (hasReplaceOrSeparator) { replacedQuery = new StringBuilder(); String queryString = query.toString(); int firstIndex = 0; for (Iterator i = queryParameters.iterator(); i.hasNext();) { QueryParameter parameter = (QueryParameter) i.next(); try { String select = parameter.getSelect(); String separator = parameter.getSeparator(); if (parameter.isReplace() || separator != null) { // Handle query modification for this parameter int secondIndex = parameter.getReplaceIndex(); replacedQuery.append(queryString.substring(firstIndex, secondIndex)); // Create List of either strings or nodes List values; if (separator == null) { // Read the expression as a string if there is a select, otherwise get parameter value as string Object objectValue; if (select != null) { objectValue = XPathUtils.selectStringValueOrNull(currentNode, parameter.getSelect(), prefixesMap, SQLFunctionLibrary.instance(), getInterpreterContext().getFunctionContextOrNull()); } else { objectValue = (parameter.getValue() == null) ? null : parameter.getValue().toString(); } values = Collections.singletonList(objectValue); } else { // Accept only a node or node-set if there is a separator, in which case a select is mandatory Object objectValue = XPathUtils.selectObjectValue(currentNode, parameter.getSelect(), prefixesMap, SQLFunctionLibrary.instance(), getInterpreterContext().getFunctionContextOrNull()); if (objectValue instanceof List) { values = (List) objectValue; } else if (objectValue instanceof Node) { values = Collections.singletonList(objectValue); } else { throw new OXFException("sql:parameter with separator requires an expression returning a node-set"); } // Set values on the parameter if they are not replaced immediately if (!parameter.isReplace()) parameter.setValues(values); } if (parameter.isReplace()) { // Replace in the query for (Iterator k = values.iterator(); k.hasNext();) { Object objectValue = k.next(); // Get value as a string String stringValue = (objectValue instanceof Node) ? XPathUtils.selectStringValue((Node) objectValue, ".") : (String) objectValue; // null values are prohibited if (stringValue == null) throw new OXFException("Cannot replace value with null result"); final String type = parameter.getType(); if (Dom4jUtils.qNameToExplodedQName(XMLConstants.XS_INT_QNAME).equals(type)) { replacedQuery.append(Integer.parseInt(stringValue)); } else if ("literal-string".equals(type) || "oxf:literalString".equals(type)) { replacedQuery.append(stringValue); } else throw new ValidationException("Unsupported parameter type: " + type, parameter.getLocationData()); // Append separator if needed if (k.hasNext()) replacedQuery.append(separator); } } else { // Update prepared statement for (int k = 0; k < values.size(); k++) { if (k > 0) replacedQuery.append(separator); replacedQuery.append(" ? "); } } firstIndex = secondIndex; } } catch (ValidationException e) { throw e; } catch (Exception e) { throw new ValidationException(e, parameter.getLocationData()); } } if (firstIndex < queryString.length()) { replacedQuery.append(queryString.substring(firstIndex)); } // We create a new PreparedStatement for each iteration String replacedQueryString = replacedQuery.toString(); if (stmt != null) { stmt.close(); } stmt = getInterpreterContext().getConnection().prepareStatement(replacedQueryString); getInterpreterContext().setStatement(stmt); getInterpreterContext().setStatementString(replacedQueryString); } // Output debug if needed if (debugString != null || SQLProcessor.logger.isDebugEnabled()) { String logMessage = "SQL statement (" + getInterpreterContext().getStatementSHA() + "): " + getInterpreterContext().getStatementString(); if (debugString != null) SQLProcessor.logger.info(logMessage); else SQLProcessor.logger.debug(logMessage); } // Set prepared statement parameters if (queryParameters != null) { int index = 1; for (Iterator i = queryParameters.iterator(); i.hasNext();) { QueryParameter parameter = (QueryParameter) i.next(); try { if (!parameter.isReplace()) { final String select = parameter.getSelect(); final String xmlType; { final String type = parameter.getType(); xmlType = GetterInterpreter.getXMLTypeFromAttributeString(getDocumentLocator(), getInterpreterContext().getPropertySet(), type, getInterpreterContext().getPrefixesMap()); } boolean doSetNull = parameter.getNullIf() != null && XPathUtils.selectBooleanValue(currentNode, parameter.getNullIf(), prefixesMap, SQLFunctionLibrary.instance(), getInterpreterContext().getFunctionContextOrNull()); if (Dom4jUtils.qNameToExplodedQName(XMLConstants.XS_STRING_QNAME).equals(xmlType) || Dom4jUtils.qNameToExplodedQName(XMLConstants.OPS_XMLFRAGMENT_QNAME).equals(xmlType)) { // Set a string or XML Fragment // List of Clobs, strings or nodes List values; if (parameter.getValues() != null) values = parameter.getValues(); else if (select != null) values = Collections.singletonList(XPathUtils.selectObjectValue(currentNode, parameter.getSelect(), prefixesMap, SQLFunctionLibrary.instance(), getInterpreterContext().getFunctionContextOrNull())); else values = Collections.singletonList(parameter.getValue()); // Iterate through all values for (Iterator k = values.iterator(); k.hasNext(); index++) { Object objectValue = k.next(); // Get Clob, String or Element Object value = null; if (!doSetNull) { if (objectValue instanceof Clob || objectValue instanceof Blob || objectValue instanceof String) { // Leave unchanged value = objectValue; } else if (Dom4jUtils.qNameToExplodedQName(XMLConstants.OPS_XMLFRAGMENT_QNAME).equals(xmlType)) { // Case of XML Fragment // Get an Element or a String if (objectValue instanceof Element) value = objectValue; else if (objectValue instanceof List) { List list = ((List) objectValue); if (list.size() == 0) value = null; else if (list.get(0) instanceof Element) value = list.get(0); else throw new OXFException("xmlFragment type expects a node-set an element node in first position"); } else if (objectValue != null) throw new OXFException("xmlFragment type expects a node, a node-set or a string"); } else { // Case of String if (objectValue instanceof Node) value = XPathUtils.selectStringValue((Node) objectValue, "."); else if (objectValue instanceof List) { List list = ((List) objectValue); if (list.size() == 0) value = null; else if (list.get(0) instanceof Node) value = XPathUtils.selectStringValue((Node) list.get(0), "."); else throw new OXFException("Invalid type: " + objectValue.getClass()); } else if (objectValue != null) throw new OXFException("Invalid type: " + objectValue.getClass()); } } final String sqlType = parameter.getSqlType(); if (value == null) { if (SQL_TYPE_CLOB.equals(sqlType)) stmt.setNull(index, Types.CLOB); else if (SQL_TYPE_BLOB.equals(sqlType)) stmt.setNull(index, Types.BLOB); else stmt.setNull(index, Types.VARCHAR); } else if (value instanceof Clob) { Clob clob = (Clob) value; if (SQL_TYPE_CLOB.equals(sqlType)) { // Set Clob as Clob stmt.setClob(index, clob); } else { // Set Clob as String long clobLength = clob.length(); if (clobLength > (long) Integer.MAX_VALUE) throw new OXFException("CLOB length can't be larger than 2GB"); stmt.setString(index, clob.getSubString(1, (int) clob.length())); } // TODO: Check BLOB: should we be able to set a String as a Blob? } else if (value instanceof String || value instanceof Element) { // Make sure we create a Document from the Element if we have one Document xmlFragmentDocument = (value instanceof Element) ? Dom4jUtils.createDocumentCopyParentNamespaces((Element) value) : null; // Convert document into an XML String if necessary if (value instanceof Element && !SQL_TYPE_XMLTYPE.equals(sqlType)) { // Convert Document into a String boolean serializeXML11 = getInterpreterContext().getPropertySet().getBoolean("serialize-xml-11", false); value = Dom4jUtils.domToString(Dom4jUtils.adjustNamespaces(xmlFragmentDocument, serializeXML11)); } if (SQL_TYPE_XMLTYPE.equals(sqlType)) { // Set DOM using native XML type if (value instanceof Element) { // We have a Document - convert it to DOM // TEMP HACK: We can't seem to be able to convert directly from dom4j to regular DOM (NAMESPACE_ERR from Xerces) // DOMResult domResult = new DOMResult(); // TransformerUtils.getIdentityTransformer().transform(new DocumentSource(xmlFragmentDocument), domResult);xxx // org.w3c.dom.Node node = domResult.getNode(); boolean serializeXML11 = getInterpreterContext().getPropertySet().getBoolean("serialize-xml-11", false); String stringValue = Dom4jUtils.domToString(Dom4jUtils.adjustNamespaces(xmlFragmentDocument, serializeXML11)); // TEMP HACK: Oracle seems to have a problem with XMLType instanciated from a DOM, so we pass a String // org.w3c.dom.Node node = XMLUtils.stringToDOM(stringValue); // if (!(node instanceof org.w3c.dom.Document)) { // // FIXME: Is this necessary? Why wouldn't we always get a Document from the transformation? // org.w3c.dom.Document document = XMLUtils.createDocument(); // document.appendChild(node); // node = document; // } // getInterpreterContext().getDelegate().setDOM(stmt, index, (org.w3c.dom.Document) node); getInterpreterContext().getDelegate().setDOM(stmt, index, stringValue); } else { // We have a String - create a DOM from it // FIXME: Do we need this? throw new UnsupportedOperationException("Setting native XML type from a String is not yet supported. Please report this usage."); } } else if (SQL_TYPE_CLOB.equals(sqlType)) { // Set String as Clob String stringValue = (String) value; //stmt.setCharacterStream(index, new StringReader(stringValue), stringValue.length()); getInterpreterContext().getDelegate().setClob(stmt, index, stringValue); // TODO: Check BLOB: should we be able to set a String as a Blob? } else { // Set String as String stmt.setString(index, (String) value); } } else throw new OXFException("Invalid parameter type: " + parameter.getType()); } } else if (Dom4jUtils.qNameToExplodedQName(XMLConstants.XS_BASE64BINARY_QNAME).equals(xmlType)) { // We are writing binary data encoded in Base 64. The only target supported // is Blob // For now, only support passing a string from the input document String sqlType = parameter.getSqlType(); if (sqlType != null && !(SQL_TYPE_BLOB.equals(sqlType) || SQL_TYPE_CLOB.equals(sqlType))) throw new OXFException("Invalid sql-type attribute: " + sqlType); if (select == null) throw new UnsupportedOperationException("Setting CLOB/BLOB requires a select attribute."); // Base64 XPathXMLReceiver xpathReceiver = getInterpreterContext().getXPathContentHandler(); if (xpathReceiver != null && xpathReceiver.containsExpression(parameter.getSelect())) { // Handle streaming if possible OutputStream blobOutputStream = getInterpreterContext().getDelegate().getBlobOutputStream(stmt, index); xpathReceiver.selectContentHandler(parameter.getSelect(), new Base64XMLReceiver(blobOutputStream)); blobOutputStream.close(); } else { String base64Value = XPathUtils.selectStringValueOrNull(currentNode, parameter.getSelect(), prefixesMap, SQLFunctionLibrary.instance(), getInterpreterContext().getFunctionContextOrNull()); getInterpreterContext().getDelegate().setBlob(stmt, index, NetUtils.base64StringToByteArray(base64Value)); } } else { // Simple cases // List of strings or nodes List values; if (parameter.getValues() != null) values = parameter.getValues(); else if (select != null) values = Collections.singletonList(XPathUtils.selectStringValueOrNull(currentNode, parameter.getSelect(), prefixesMap, SQLFunctionLibrary.instance(), getInterpreterContext().getFunctionContextOrNull())); else values = Collections.singletonList(parameter.getValue()); // Iterate through all values for (Iterator k = values.iterator(); k.hasNext(); index++) { Object objectValue = k.next(); // Get String value String stringValue = null; if (!doSetNull) { if (objectValue instanceof String) stringValue = (String) objectValue; else if (objectValue != null) stringValue = XPathUtils.selectStringValue((Node) objectValue, "."); } // For the specific type, set to null or convert String value if (Dom4jUtils.qNameToExplodedQName(XMLConstants.XS_INT_QNAME).equals(xmlType)) { if (stringValue == null) stmt.setNull(index, Types.INTEGER); else stmt.setInt(index, Integer.parseInt(stringValue)); } else if (Dom4jUtils.qNameToExplodedQName(XMLConstants.XS_DATE_QNAME).equals(xmlType)) { if (stringValue == null) { stmt.setNull(index, Types.DATE); } else { java.sql.Date date = new java.sql.Date(DateUtils.parseISODateOrDateTime(stringValue)); stmt.setDate(index, date); } } else if (Dom4jUtils.qNameToExplodedQName(XMLConstants.XS_DATETIME_QNAME).equals(xmlType)) { if (stringValue == null) { stmt.setNull(index, Types.TIMESTAMP); } else { java.sql.Timestamp timestamp = new java.sql.Timestamp(DateUtils.parseISODateOrDateTime(stringValue)); stmt.setTimestamp(index, timestamp); } } else if (Dom4jUtils.qNameToExplodedQName(XMLConstants.XS_BOOLEAN_QNAME).equals(xmlType)) { if (stringValue == null) stmt.setNull(index, Types.BOOLEAN); else stmt.setBoolean(index, "true".equals(stringValue)); } else if (Dom4jUtils.qNameToExplodedQName(XMLConstants.XS_DECIMAL_QNAME).equals(xmlType)) { if (stringValue == null) stmt.setNull(index, Types.DECIMAL); else stmt.setBigDecimal(index, new BigDecimal(stringValue)); } else if (Dom4jUtils.qNameToExplodedQName(XMLConstants.XS_FLOAT_QNAME).equals(xmlType)) { if (stringValue == null) stmt.setNull(index, Types.FLOAT); else stmt.setFloat(index, Float.parseFloat(stringValue)); } else if (Dom4jUtils.qNameToExplodedQName(XMLConstants.XS_DOUBLE_QNAME).equals(xmlType)) { if (stringValue == null) stmt.setNull(index, Types.DOUBLE); else stmt.setDouble(index, Double.parseDouble(stringValue)); } else if (Dom4jUtils.qNameToExplodedQName(XMLConstants.XS_ANYURI_QNAME).equals(xmlType)) { String sqlType = parameter.getSqlType(); if (sqlType != null && !(SQL_TYPE_BLOB.equals(sqlType) || SQL_TYPE_CLOB.equals(sqlType))) throw new OXFException("Invalid sql-type attribute: " + sqlType); if (stringValue == null) { stmt.setNull(index, Types.BLOB); } else { // Dereference the URI and write to the BLOB OutputStream blobOutputStream = getInterpreterContext().getDelegate().getBlobOutputStream(stmt, index); NetUtils.anyURIToOutputStream(stringValue, blobOutputStream); blobOutputStream.close(); } } else throw new ValidationException("Unsupported parameter type: " + type, parameter.getLocationData()); } } } } catch (ValidationException e) { throw e; } catch (Exception e) { throw new ValidationException(e, parameter.getLocationData()); } } } } finally { getInterpreterContext().popFunctionContext(); } if (type == QUERY || type == CALL) { if (nodeCount > 1) throw new ValidationException("More than one iteration on sql:query or sql:call element", new LocationData(getDocumentLocator())); // Execute if (SQLProcessor.logger.isDebugEnabled()) SQLProcessor.logger.debug("Executing query/call, " + "statement = " + getInterpreterContext().getStatementSHA()); final boolean hasResultSet = stmt.execute(); ResultSetInterpreter.setResultSetInfo(getInterpreterContext(), stmt, hasResultSet); } else if (type == UPDATE) { // We know there is only a possible update count final int updateCount = stmt.executeUpdate(); getInterpreterContext().setUpdateCount(updateCount);//FIXME: should add? if (updateCount > 0) ResultSetInterpreter.setGeneratedKeysResultSetInfo(getInterpreterContext(), stmt); } } } catch (Exception e) { // FIXME: should store exception so that it can be retrieved // Actually, we'll need a global exception mechanism for pipelines, so this may end up being done // in XPL or BPEL. // Log closest query related to the exception if we can find it String statementString = getInterpreterContext().getStatementString(); SQLProcessor.logger.error("PreparedStatement:\n" + statementString); // And throw throw new ValidationException(e, new LocationData(getDocumentLocator())); } } private static class QueryParameter { private String direction; private String type; private String sqlType; private String select; private String separator; private boolean replace; private String nullIf; private int replaceIndex; private Object value; private List values; private LocationData locationData; public QueryParameter(String direction, String type, String sqlType, String select, String separator, boolean replace, String nullIf, int replaceIndex, LocationData locationData) { this.direction = direction; this.type = type; this.sqlType = sqlType; this.select = select; this.separator = separator; this.replace = replace; this.nullIf = nullIf; this.replaceIndex = replaceIndex; this.locationData = locationData; } public void setValue(Object value) { this.value = value; } public String getDirection() { return direction; } public String getType() { return type; } public String getSqlType() { return sqlType; } public String getSelect() { return select; } public String getSeparator() { return separator; } public boolean isReplace() { return replace; } public String getNullIf() { return nullIf; } public int getReplaceIndex() { return replaceIndex; } public Object getValue() { return value; } public LocationData getLocationData() { return locationData; } public List getValues() { return values; } public void setValues(List values) { this.values = values; } } }