You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@metamodel.apache.org by ka...@apache.org on 2013/07/22 10:11:03 UTC
[50/64] [partial] Hard rename of all 'org/eobjects' folders to
'org/apache'.
http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/AbstractDataContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/AbstractDataContext.java b/core/src/main/java/org/apache/metamodel/AbstractDataContext.java
new file mode 100644
index 0000000..3300e17
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/AbstractDataContext.java
@@ -0,0 +1,463 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eobjects.metamodel;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.eobjects.metamodel.data.DataSet;
+import org.eobjects.metamodel.query.CompiledQuery;
+import org.eobjects.metamodel.query.DefaultCompiledQuery;
+import org.eobjects.metamodel.query.Query;
+import org.eobjects.metamodel.query.builder.InitFromBuilder;
+import org.eobjects.metamodel.query.builder.InitFromBuilderImpl;
+import org.eobjects.metamodel.query.parser.QueryParser;
+import org.eobjects.metamodel.schema.Column;
+import org.eobjects.metamodel.schema.Schema;
+import org.eobjects.metamodel.schema.Table;
+
+/**
+ * Abstract implementation of the DataContext interface. Provides convenient
+ * implementations of all trivial and datastore-independent methods.
+ *
+ * @author Kasper Sørensen
+ */
+public abstract class AbstractDataContext implements DataContext {
+
+ private static final String NULL_SCHEMA_NAME_TOKEN = "<metamodel.schema.name.null>";
+ private final ConcurrentMap<String, Schema> _schemaCache = new ConcurrentHashMap<String, Schema>();
+ private final Comparator<? super String> _schemaNameComparator = SchemaNameComparator.getInstance();
+ private String[] _schemaNameCache;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final DataContext refreshSchemas() {
+ _schemaCache.clear();
+ _schemaNameCache = null;
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final Schema[] getSchemas() throws MetaModelException {
+ String[] schemaNames = getSchemaNames();
+ Schema[] schemas = new Schema[schemaNames.length];
+ for (int i = 0; i < schemaNames.length; i++) {
+ final String name = schemaNames[i];
+ final Schema schema = _schemaCache.get(getSchemaCacheKey(name));
+ if (schema == null) {
+ final Schema newSchema = getSchemaByName(name);
+ if (newSchema == null) {
+ throw new MetaModelException("Declared schema does not exist: " + name);
+ }
+ final Schema existingSchema = _schemaCache.putIfAbsent(getSchemaCacheKey(name), newSchema);
+ if (existingSchema == null) {
+ schemas[i] = newSchema;
+ } else {
+ schemas[i] = existingSchema;
+ }
+ } else {
+ schemas[i] = schema;
+ }
+ }
+ return schemas;
+ }
+
+ private String getSchemaCacheKey(String name) {
+ if (name == null) {
+ return NULL_SCHEMA_NAME_TOKEN;
+ }
+ return name;
+ }
+
+ /**
+ * m {@inheritDoc}
+ */
+ @Override
+ public final String[] getSchemaNames() throws MetaModelException {
+ if (_schemaNameCache == null) {
+ _schemaNameCache = getSchemaNamesInternal();
+ }
+ String[] schemaNames = Arrays.copyOf(_schemaNameCache, _schemaNameCache.length);
+ Arrays.sort(schemaNames, _schemaNameComparator);
+ return schemaNames;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final Schema getDefaultSchema() throws MetaModelException {
+ Schema result = null;
+ String defaultSchemaName = getDefaultSchemaName();
+ if (defaultSchemaName != null) {
+ result = getSchemaByName(defaultSchemaName);
+ }
+ if (result == null) {
+ Schema[] schemas = getSchemas();
+ if (schemas.length == 1) {
+ result = schemas[0];
+ } else {
+ int highestTableCount = -1;
+ for (int i = 0; i < schemas.length; i++) {
+ final Schema schema = schemas[i];
+ String name = schema.getName();
+ if (schema != null) {
+ name = name.toLowerCase();
+ boolean isInformationSchema = name.startsWith("information") && name.endsWith("schema");
+ if (!isInformationSchema && schema.getTableCount() > highestTableCount) {
+ highestTableCount = schema.getTableCount();
+ result = schema;
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final InitFromBuilder query() {
+ return new InitFromBuilderImpl(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Query parseQuery(final String queryString) throws MetaModelException {
+ final QueryParser parser = new QueryParser(this, queryString);
+ final Query query = parser.parse();
+ return query;
+ }
+
+ @Override
+ public CompiledQuery compileQuery(final Query query) throws MetaModelException {
+ return new DefaultCompiledQuery(query);
+ }
+
+ @Override
+ public DataSet executeQuery(CompiledQuery compiledQuery, Object... values) {
+ assert compiledQuery instanceof DefaultCompiledQuery;
+
+ final DefaultCompiledQuery defaultCompiledQuery = (DefaultCompiledQuery) compiledQuery;
+ final Query query = defaultCompiledQuery.cloneWithParameterValues(values);
+
+ return executeQuery(query);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final DataSet executeQuery(final String queryString) throws MetaModelException {
+ final Query query = parseQuery(queryString);
+ final DataSet dataSet = executeQuery(query);
+ return dataSet;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final Schema getSchemaByName(String name) throws MetaModelException {
+ Schema schema = _schemaCache.get(getSchemaCacheKey(name));
+ if (schema == null) {
+ if (name == null) {
+ schema = getSchemaByNameInternal(null);
+ } else {
+ String[] schemaNames = getSchemaNames();
+ for (String schemaName : schemaNames) {
+ if (name.equalsIgnoreCase(schemaName)) {
+ schema = getSchemaByNameInternal(name);
+ break;
+ }
+ }
+ if (schema == null) {
+ for (String schemaName : schemaNames) {
+ if (name.equalsIgnoreCase(schemaName)) {
+ // try again with "schemaName" as param instead of
+ // "name".
+ schema = getSchemaByNameInternal(schemaName);
+ break;
+ }
+ }
+ }
+ }
+ if (schema != null) {
+ Schema existingSchema = _schemaCache.putIfAbsent(getSchemaCacheKey(schema.getName()), schema);
+ if (existingSchema != null) {
+ // race conditions may cause two schemas to be created.
+ // We'll favor the existing schema if possible, since schema
+ // may contain lazy-loading logic and so on.
+ return existingSchema;
+ }
+ }
+ }
+ return schema;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final Column getColumnByQualifiedLabel(final String columnName) {
+ if (columnName == null) {
+ return null;
+ }
+ Schema schema = null;
+ final String[] schemaNames = getSchemaNames();
+ for (final String schemaName : schemaNames) {
+ if (schemaName == null) {
+ // search without schema name (some databases have only a single
+ // schema with no name)
+ schema = getSchemaByName(null);
+ if (schema != null) {
+ Column column = getColumn(schema, columnName);
+ if (column != null) {
+ return column;
+ }
+ }
+ } else {
+ // Search case-sensitive
+ Column col = searchColumn(schemaName, columnName, columnName);
+ if (col != null) {
+ return col;
+ }
+ }
+ }
+
+ final String columnNameInLowerCase = columnName.toLowerCase();
+ for (final String schemaName : schemaNames) {
+ if (schemaName != null) {
+ // search case-insensitive
+ String schameNameInLowerCase = schemaName.toLowerCase();
+ Column col = searchColumn(schameNameInLowerCase, columnName, columnNameInLowerCase);
+ if (col != null) {
+ return col;
+ }
+ }
+ }
+
+ schema = getDefaultSchema();
+ if (schema != null) {
+ Column column = getColumn(schema, columnName);
+ if (column != null) {
+ return column;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Searches for a particular column within a schema
+ *
+ * @param schemaNameSearch
+ * the schema name to use for search
+ * @param columnNameOriginal
+ * the original column name
+ * @param columnNameSearch
+ * the column name as it should be searched for (either the same
+ * as original, or lower case in case of case-insensitive search)
+ * @return
+ */
+ private Column searchColumn(String schemaNameSearch, String columnNameOriginal, String columnNameSearch) {
+ if (columnNameSearch.startsWith(schemaNameSearch)) {
+ Schema schema = getSchemaByName(schemaNameSearch);
+ if (schema != null) {
+ String tableAndColumnPath = columnNameOriginal.substring(schemaNameSearch.length());
+
+ if (tableAndColumnPath.charAt(0) == '.') {
+ tableAndColumnPath = tableAndColumnPath.substring(1);
+
+ Column column = getColumn(schema, tableAndColumnPath);
+ if (column != null) {
+ return column;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private final Column getColumn(final Schema schema, final String tableAndColumnPath) {
+ Table table = null;
+ String columnPath = tableAndColumnPath;
+ final String[] tableNames = schema.getTableNames();
+ for (final String tableName : tableNames) {
+ if (tableName != null) {
+ // search case-sensitive
+ if (isStartingToken(tableName, tableAndColumnPath)) {
+ table = schema.getTableByName(tableName);
+ columnPath = tableAndColumnPath.substring(tableName.length());
+
+ if (columnPath.charAt(0) == '.') {
+ columnPath = columnPath.substring(1);
+ break;
+ }
+ }
+ }
+ }
+
+ if (table == null) {
+ final String tableAndColumnPathInLowerCase = tableAndColumnPath.toLowerCase();
+ for (final String tableName : tableNames) {
+ if (tableName != null) {
+ String tableNameInLowerCase = tableName.toLowerCase();
+ // search case-insensitive
+ if (isStartingToken(tableNameInLowerCase, tableAndColumnPathInLowerCase)) {
+ table = schema.getTableByName(tableName);
+ columnPath = tableAndColumnPath.substring(tableName.length());
+
+ if (columnPath.charAt(0) == '.') {
+ columnPath = columnPath.substring(1);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (table == null && tableNames.length == 1) {
+ table = schema.getTables()[0];
+ }
+
+ if (table != null) {
+ Column column = table.getColumnByName(columnPath);
+ if (column != null) {
+ return column;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final Table getTableByQualifiedLabel(final String tableName) {
+ if (tableName == null) {
+ return null;
+ }
+ Schema schema = null;
+ String[] schemaNames = getSchemaNames();
+ for (String schemaName : schemaNames) {
+ if (schemaName == null) {
+ // there's an unnamed schema present.
+ schema = getSchemaByName(null);
+ if (schema != null) {
+ Table table = schema.getTableByName(tableName);
+ return table;
+ }
+ } else {
+ // case-sensitive search
+ if (isStartingToken(schemaName, tableName)) {
+ schema = getSchemaByName(schemaName);
+ }
+ }
+ }
+
+ if (schema == null) {
+ final String tableNameInLowerCase = tableName.toLowerCase();
+ for (final String schemaName : schemaNames) {
+ if (schemaName != null) {
+ // case-insensitive search
+ final String schemaNameInLowerCase = schemaName.toLowerCase();
+ if (isStartingToken(schemaNameInLowerCase, tableNameInLowerCase)) {
+ schema = getSchemaByName(schemaName);
+ }
+ }
+ }
+ }
+
+ if (schema == null) {
+ schema = getDefaultSchema();
+ }
+
+ String tablePart = tableName.toLowerCase();
+ String schemaName = schema.getName();
+ if (schemaName != null && isStartingToken(schemaName.toLowerCase(), tablePart)) {
+ tablePart = tablePart.substring(schemaName.length());
+ if (tablePart.startsWith(".")) {
+ tablePart = tablePart.substring(1);
+ }
+ }
+
+ return schema.getTableByName(tablePart);
+ }
+
+ private boolean isStartingToken(String partName, String fullName) {
+ if (fullName.startsWith(partName)) {
+ final int length = partName.length();
+ if (length == 0) {
+ return false;
+ }
+ if (fullName.length() > length) {
+ final char nextChar = fullName.charAt(length);
+ if (isQualifiedPathDelim(nextChar)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ protected boolean isQualifiedPathDelim(char c) {
+ return c == '.' || c == '"';
+ }
+
+ /**
+ * Gets schema names from the non-abstract implementation. These schema
+ * names will be cached except if the {@link #refreshSchemas()} method is
+ * called.
+ *
+ * @return an array of schema names.
+ */
+ protected abstract String[] getSchemaNamesInternal();
+
+ /**
+ * Gets the name of the default schema.
+ *
+ * @return the default schema name.
+ */
+ protected abstract String getDefaultSchemaName();
+
+ /**
+ * Gets a specific schema from the non-abstract implementation. This schema
+ * object will be cached except if the {@link #refreshSchemas()} method is
+ * called.
+ *
+ * @param name
+ * the name of the schema to get
+ * @return a schema object representing the named schema, or null if no such
+ * schema exists.
+ */
+ protected abstract Schema getSchemaByNameInternal(String name);
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/AbstractUpdateCallback.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/AbstractUpdateCallback.java b/core/src/main/java/org/apache/metamodel/AbstractUpdateCallback.java
new file mode 100644
index 0000000..602a412
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/AbstractUpdateCallback.java
@@ -0,0 +1,164 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eobjects.metamodel;
+
+import java.util.Arrays;
+
+import org.eobjects.metamodel.create.TableCreationBuilder;
+import org.eobjects.metamodel.delete.RowDeletionBuilder;
+import org.eobjects.metamodel.drop.TableDropBuilder;
+import org.eobjects.metamodel.insert.RowInsertionBuilder;
+import org.eobjects.metamodel.schema.Schema;
+import org.eobjects.metamodel.schema.Table;
+import org.eobjects.metamodel.update.RowUpdationBuilder;
+
+/**
+ * Abstract implementation of the {@link UpdateCallback} interface. Implements
+ * only the data store agnostic methods.
+ *
+ * @author Kasper Sørensen
+ */
+public abstract class AbstractUpdateCallback implements UpdateCallback {
+
+ private final DataContext _dataContext;
+
+ public AbstractUpdateCallback(DataContext dataContext) {
+ _dataContext = dataContext;
+ }
+
+ @Override
+ public TableCreationBuilder createTable(String schemaName, String tableName) throws IllegalArgumentException,
+ IllegalStateException {
+ Schema schema = getSchema(schemaName);
+ return createTable(schema, tableName);
+ }
+
+ @Override
+ public TableDropBuilder dropTable(String schemaName, String tableName) throws IllegalArgumentException,
+ IllegalStateException, UnsupportedOperationException {
+ Table table = getTable(schemaName, tableName);
+ return dropTable(table);
+ }
+
+ @Override
+ public TableDropBuilder dropTable(Schema schema, String tableName) throws IllegalArgumentException,
+ IllegalStateException, UnsupportedOperationException {
+ Table table = schema.getTableByName(tableName);
+ if (table == null) {
+ throw new IllegalArgumentException("Nu such table '" + tableName + "' found in schema: " + schema
+ + ". Available tables are: " + Arrays.toString(schema.getTableNames()));
+ }
+ return dropTable(table);
+ }
+
+ @Override
+ public final RowInsertionBuilder insertInto(String tableName) throws IllegalArgumentException,
+ IllegalStateException {
+ return insertInto(getTable(tableName));
+ }
+
+ @Override
+ public RowInsertionBuilder insertInto(String schemaName, String tableName) throws IllegalArgumentException,
+ IllegalStateException, UnsupportedOperationException {
+ return insertInto(getTable(schemaName, tableName));
+ }
+
+ private Table getTable(String schemaName, String tableName) {
+ final Schema schema = getSchema(schemaName);
+ final Table table = schema.getTableByName(tableName);
+ if (table == null) {
+ throw new IllegalArgumentException("Nu such table '" + tableName + "' found in schema: " + schema
+ + ". Available tables are: " + Arrays.toString(schema.getTableNames()));
+ }
+ return table;
+ }
+
+ private Schema getSchema(String schemaName) {
+ final Schema schema = _dataContext.getSchemaByName(schemaName);
+ if (schema == null) {
+ throw new IllegalArgumentException("No such schema: " + schemaName);
+ }
+ return schema;
+ }
+
+ @Override
+ public final RowDeletionBuilder deleteFrom(String tableName) {
+ return deleteFrom(getTable(tableName));
+ }
+
+ @Override
+ public RowDeletionBuilder deleteFrom(String schemaName, String tableName) throws IllegalArgumentException,
+ IllegalStateException, UnsupportedOperationException {
+ final Table table = getTable(schemaName, tableName);
+ return deleteFrom(table);
+ }
+
+ @Override
+ public final TableDropBuilder dropTable(String tableName) {
+ return dropTable(getTable(tableName));
+ }
+
+ @Override
+ public final RowUpdationBuilder update(String tableName) {
+ return update(getTable(tableName));
+ }
+
+ private Table getTable(String tableName) {
+ Table table = getDataContext().getTableByQualifiedLabel(tableName);
+ if (table == null) {
+ throw new IllegalArgumentException("No such table: " + tableName);
+ }
+ return table;
+ }
+
+ @Override
+ public DataContext getDataContext() {
+ return _dataContext;
+ }
+
+ @Override
+ public boolean isCreateTableSupported() {
+ // since 2.0 all updateable datacontext have create table support
+ return true;
+ }
+
+ @Override
+ public boolean isInsertSupported() {
+ // since 2.0 all updateable datacontext have create table support
+ return true;
+ }
+
+ @Override
+ public boolean isUpdateSupported() {
+ return isInsertSupported() && isDeleteSupported();
+ }
+
+ @Override
+ public RowUpdationBuilder update(String schemaName, String tableName) throws IllegalArgumentException,
+ IllegalStateException, UnsupportedOperationException {
+ final Table table = getTable(schemaName, tableName);
+ return update(table);
+ }
+
+ @Override
+ public RowUpdationBuilder update(Table table) throws IllegalArgumentException, IllegalStateException,
+ UnsupportedOperationException {
+ return new DeleteAndInsertBuilder(this, table);
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/BatchUpdateScript.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/BatchUpdateScript.java b/core/src/main/java/org/apache/metamodel/BatchUpdateScript.java
new file mode 100644
index 0000000..d7b567c
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/BatchUpdateScript.java
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eobjects.metamodel;
+
+/**
+ * Indicator sub-interface of {@link UpdateScript}. Implementing your updates
+ * using this interface indicates to the underlying
+ * {@link UpdateableDataContext} that the update script represents a large batch
+ * update and that appropriate optimizations may be taken into use if available.
+ */
+public interface BatchUpdateScript extends UpdateScript {
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/CompositeDataContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/CompositeDataContext.java b/core/src/main/java/org/apache/metamodel/CompositeDataContext.java
new file mode 100644
index 0000000..82d6c01
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/CompositeDataContext.java
@@ -0,0 +1,204 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eobjects.metamodel;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eobjects.metamodel.data.DataSet;
+import org.eobjects.metamodel.query.FromItem;
+import org.eobjects.metamodel.query.Query;
+import org.eobjects.metamodel.schema.CompositeSchema;
+import org.eobjects.metamodel.schema.Schema;
+import org.eobjects.metamodel.schema.Table;
+import org.eobjects.metamodel.util.Func;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * DataContext for composite datacontexts. Composite DataContexts wrap several
+ * other datacontexts and makes cross-datastore querying possible.
+ *
+ * @author Kasper Sørensen
+ */
+public class CompositeDataContext extends AbstractDataContext {
+
+ private final static Logger logger = LoggerFactory.getLogger(CompositeDataContext.class);
+ private Map<String, CompositeSchema> _compositeSchemas = new HashMap<String, CompositeSchema>();
+ private DataContext[] _delegates;
+
+ public CompositeDataContext(DataContext... delegates) {
+ if (delegates == null) {
+ throw new IllegalArgumentException("delegates cannot be null");
+ }
+ _delegates = delegates;
+ }
+
+ public CompositeDataContext(Collection<DataContext> delegates) {
+ if (delegates == null) {
+ throw new IllegalArgumentException("delegates cannot be null");
+ }
+ _delegates = delegates.toArray(new DataContext[delegates.size()]);
+ }
+
+ @Override
+ public DataSet executeQuery(Query query) throws MetaModelException {
+ // a set of all datacontexts involved
+ Set<DataContext> dataContexts = new HashSet<DataContext>();
+
+ // find all datacontexts involved, by investigating FROM items
+ List<FromItem> items = query.getFromClause().getItems();
+ for (FromItem item : items) {
+ List<FromItem> tableFromItems = MetaModelHelper.getTableFromItems(item);
+ for (FromItem fromItem : tableFromItems) {
+ Table table = fromItem.getTable();
+
+ DataContext dc = getDataContext(table);
+ if (dc == null) {
+ throw new MetaModelException("Could not resolve child-datacontext for table: " + table);
+ }
+ dataContexts.add(dc);
+ }
+ }
+
+ if (dataContexts.isEmpty()) {
+ throw new MetaModelException("No suiting delegate DataContext to execute query: " + query);
+ } else if (dataContexts.size() == 1) {
+ Iterator<DataContext> it = dataContexts.iterator();
+ assert it.hasNext();
+ DataContext dc = it.next();
+ return dc.executeQuery(query);
+ } else {
+ // we create a datacontext which can materialize tables from
+ // separate datacontexts.
+ final Func<Table, DataContext> dataContextRetrievalFunction = new Func<Table, DataContext>() {
+ @Override
+ public DataContext eval(Table table) {
+ return getDataContext(table);
+ }
+ };
+ return new CompositeQueryDelegate(dataContextRetrievalFunction).executeQuery(query);
+ }
+ }
+
+ private DataContext getDataContext(Table table) {
+ DataContext result = null;
+ if (table != null) {
+ Schema schema = table.getSchema();
+
+ if (schema != null) {
+ for (DataContext dc : _delegates) {
+ Schema dcSchema = dc.getSchemaByName(schema.getName());
+ if (dcSchema != null) {
+
+ // first round = try with schema identity match
+ if (dcSchema == schema) {
+ logger.debug("DataContext for '{}' resolved (using identity) to: '{}'", table, dcSchema);
+ result = dc;
+ break;
+ }
+ }
+ }
+
+ if (result == null) {
+ for (DataContext dc : _delegates) {
+ Schema dcSchema = dc.getSchemaByName(schema.getName());
+ if (dcSchema != null) {
+ // second round = try with schema equals method
+ if (dcSchema.equals(schema)) {
+ logger.debug("DataContext for '{}' resolved (using equals) to: '{}'", table, dcSchema);
+ result = dc;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (result == null) {
+ logger.warn("Couldn't resolve DataContext for {}", table);
+ }
+ return result;
+ }
+
+ @Override
+ public String getDefaultSchemaName() throws MetaModelException {
+ for (DataContext dc : _delegates) {
+ Schema schema = dc.getDefaultSchema();
+ if (schema != null) {
+ return schema.getName();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Schema getSchemaByNameInternal(String name) throws MetaModelException {
+ CompositeSchema compositeSchema = _compositeSchemas.get(name);
+ if (compositeSchema != null) {
+ return compositeSchema;
+ }
+ List<Schema> matchingSchemas = new ArrayList<Schema>();
+ for (DataContext dc : _delegates) {
+ Schema schema = dc.getSchemaByName(name);
+ if (schema != null) {
+ matchingSchemas.add(schema);
+ }
+ }
+ if (matchingSchemas.size() == 1) {
+ return matchingSchemas.iterator().next();
+ }
+ if (matchingSchemas.size() > 1) {
+ if (logger.isInfoEnabled()) {
+ logger.info("Name-clash detected for Schema '{}'. Creating CompositeSchema.");
+ }
+ compositeSchema = new CompositeSchema(name, matchingSchemas);
+ _compositeSchemas.put(name, compositeSchema);
+ return compositeSchema;
+ }
+ return null;
+ }
+
+ @Override
+ public String[] getSchemaNamesInternal() throws MetaModelException {
+ Set<String> set = new HashSet<String>();
+ for (DataContext dc : _delegates) {
+ String[] schemaNames = dc.getSchemaNames();
+ for (String name : schemaNames) {
+ if (!MetaModelHelper.isInformationSchema(name)) {
+ // we skip information schemas, since they're anyways going
+ // to be incomplete and misleading.
+ set.add(name);
+ }
+ }
+ }
+ String[] result = set.toArray(new String[set.size()]);
+ Arrays.sort(result);
+ return result;
+ }
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/CompositeQueryDelegate.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/CompositeQueryDelegate.java b/core/src/main/java/org/apache/metamodel/CompositeQueryDelegate.java
new file mode 100644
index 0000000..70b19f1
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/CompositeQueryDelegate.java
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eobjects.metamodel;
+
+import org.eobjects.metamodel.data.DataSet;
+import org.eobjects.metamodel.query.Query;
+import org.eobjects.metamodel.schema.Column;
+import org.eobjects.metamodel.schema.Table;
+import org.eobjects.metamodel.util.Func;
+
+final class CompositeQueryDelegate extends QueryPostprocessDelegate {
+
+ private final Func<Table, DataContext> _dataContextRetrievalFunction;
+
+ public CompositeQueryDelegate(
+ Func<Table, DataContext> dataContextRetrievalFunction) {
+ _dataContextRetrievalFunction = dataContextRetrievalFunction;
+ }
+
+ @Override
+ protected DataSet materializeMainSchemaTable(Table table, Column[] columns,
+ int maxRows) {
+ // find the appropriate datacontext to execute a simple
+ // table materialization query
+ DataContext dc = _dataContextRetrievalFunction.eval(table);
+ Query q = new Query().select(columns).from(table);
+ if (maxRows >= 0) {
+ q.setMaxRows(maxRows);
+ }
+ return dc.executeQuery(q);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/DataContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/DataContext.java b/core/src/main/java/org/apache/metamodel/DataContext.java
new file mode 100644
index 0000000..b536d7d
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/DataContext.java
@@ -0,0 +1,199 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eobjects.metamodel;
+
+import org.eobjects.metamodel.data.DataSet;
+import org.eobjects.metamodel.query.CompiledQuery;
+import org.eobjects.metamodel.query.Query;
+import org.eobjects.metamodel.query.QueryParameter;
+import org.eobjects.metamodel.query.builder.InitFromBuilder;
+import org.eobjects.metamodel.schema.Column;
+import org.eobjects.metamodel.schema.Schema;
+import org.eobjects.metamodel.schema.Table;
+
+/**
+ * A DataContext represents the central entry point for interactions with
+ * datastores. The DataContext contains of the structure of data (in the form of
+ * schemas) and interactions (in the form of queries) with data.
+ *
+ * @author Kasper Sørensen
+ */
+public interface DataContext {
+
+ /**
+ * Enforces a refresh of the schemas. If not refreshed, cached schema
+ * objects may be used.
+ *
+ * @return this DataContext
+ */
+ public DataContext refreshSchemas();
+
+ /**
+ * Gets all schemas within this DataContext.
+ *
+ * @return the schemas in this data context. Schemas are cached for reuse in
+ * many situations so if you want to update the schemas, use the
+ * refreshSchemas() method.
+ * @throws MetaModelException
+ * if an error occurs retrieving the schema model
+ */
+ public Schema[] getSchemas() throws MetaModelException;
+
+ /**
+ * Gets the names of all schemas within this DataContext.
+ *
+ * @return an array of valid schema names
+ * @throws MetaModelException
+ * if an error occurs retrieving the schema model
+ */
+ public String[] getSchemaNames() throws MetaModelException;
+
+ /**
+ * Gets the default schema of this DataContext.
+ *
+ * @return the schema that you are most probable to be interested in. The
+ * default schema is determined by finding the schema with most
+ * available of tables. In a lot of situations there will only be a
+ * single available schema and in that case this will of course be
+ * the schema returned.
+ * @throws MetaModelException
+ * if an error occurs retrieving the schema model
+ */
+ public Schema getDefaultSchema() throws MetaModelException;
+
+ /**
+ * Gets a schema by a specified name.
+ *
+ * @param name
+ * the name of the desired schema
+ * @return the Schema with the specified name or null if no such schema
+ * exists
+ * @throws MetaModelException
+ * if an error occurs retrieving the schema model
+ */
+ public Schema getSchemaByName(String name) throws MetaModelException;
+
+ /**
+ * Starts building a query using the query builder API. This way of building
+ * queries is the preferred approach since it provides a more type-safe
+ * approach to building API's as well as allows the DataContext
+ * implementation to be aware of the query building process.
+ *
+ * @return a query builder component at the initial position in building a
+ * query.
+ */
+ public InitFromBuilder query();
+
+ /**
+ * Parses a string-based SQL query and produces a corresponding
+ * {@link Query} object.
+ *
+ * @param queryString
+ * the SQL query to parse
+ * @return a {@link Query} object corresponding to the SQL query.
+ * @throws MetaModelException
+ * in case the parsing was unsuccesful.
+ */
+ public Query parseQuery(String queryString) throws MetaModelException;
+
+ /**
+ * Executes a query against the DataContext.
+ *
+ * @param query
+ * the query object to execute
+ * @return the {@link DataSet} produced from executing the query
+ * @throws MetaModelException
+ * if the specified query does not make sense or cannot be
+ * executed because of restraints on the type of datastore.
+ */
+ public DataSet executeQuery(Query query) throws MetaModelException;
+
+ /**
+ * Compiles a query, preparing it for reuse. Often times compiled queries
+ * have a performance improvement when executed, but at the cost of a
+ * preparation time penalty. Therefore it is adviced to use compiled queries
+ * when the same query is to be fired multiple times.
+ *
+ * Compiled queries can contain {@link QueryParameter}s as operands in the
+ * WHERE clause, making it possible to reuse the same query with different
+ * parameter values.
+ *
+ * @see CompiledQuery
+ * @see QueryParameter
+ *
+ * @param query
+ * the query object to execute, possibly holding one or more
+ * {@link QueryParameter}s.
+ * @return the {@link CompiledQuery} after preparing the query
+ *
+ * @throws MetaModelException
+ * if preparing the query is unsuccesful
+ */
+ public CompiledQuery compileQuery(Query query) throws MetaModelException;
+
+ /**
+ * Executes a compiled query with given values as parameters.
+ *
+ * @param compiledQuery
+ * the compiledQuery object to execute
+ * @param values
+ * the values for parameters in the {@link CompiledQuery}.
+ * @return the {@link DataSet} produced from executing the query.
+ */
+ public DataSet executeQuery(CompiledQuery compiledQuery, Object... values);
+
+ /**
+ * Parses and executes a string-based SQL query.
+ *
+ * This method is essentially equivalent to calling first
+ * {@link #parseQuery(String)} and then {@link #executeQuery(Query)} with
+ * the parsed query.
+ *
+ * @param query
+ * the SQL query to parse
+ * @return the {@link DataSet} produced from executing the query
+ * @throws MetaModelException
+ * if either parsing or executing the query produces an
+ * exception
+ */
+ public DataSet executeQuery(String queryString) throws MetaModelException;
+
+ /**
+ * Finds a column in the DataContext based on a fully qualified column
+ * label. The qualified label consists of the the schema, table and column
+ * name, delimited by a dot (.).
+ *
+ * @param columnName
+ * @return a column that matches the qualified label, or null if no such
+ * column exists
+ */
+ public Column getColumnByQualifiedLabel(final String columnName);
+
+ /**
+ * Finds a table in the DataContext based on a fully qualified table label.
+ * The qualified label consists of the the schema and table name, delimited
+ * by a dot (.).
+ *
+ * @param tableName
+ * @return a table that matches the qualified label, or null if no such
+ * table exists
+ */
+ public Table getTableByQualifiedLabel(final String tableName);
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/DeleteAndInsertBuilder.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/DeleteAndInsertBuilder.java b/core/src/main/java/org/apache/metamodel/DeleteAndInsertBuilder.java
new file mode 100644
index 0000000..36e4b3e
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/DeleteAndInsertBuilder.java
@@ -0,0 +1,111 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eobjects.metamodel;
+
+import java.util.List;
+import java.util.ListIterator;
+
+import org.eobjects.metamodel.data.DataSet;
+import org.eobjects.metamodel.data.DefaultRow;
+import org.eobjects.metamodel.data.Row;
+import org.eobjects.metamodel.data.SimpleDataSetHeader;
+import org.eobjects.metamodel.query.FilterItem;
+import org.eobjects.metamodel.query.SelectItem;
+import org.eobjects.metamodel.schema.Column;
+import org.eobjects.metamodel.schema.Table;
+import org.eobjects.metamodel.update.AbstractRowUpdationBuilder;
+import org.eobjects.metamodel.update.RowUpdationBuilder;
+
+/**
+ * Simple implementation of the {@link RowUpdationBuilder} interface, which
+ * simply uses a combined delete+insert strategy for performing updates. Note
+ * that this implementation is not desirable performance-wise in many cases, but
+ * does provide a functional equivalent to a "real" update.
+ */
+public class DeleteAndInsertBuilder extends AbstractRowUpdationBuilder {
+
+ private final AbstractUpdateCallback _updateCallback;
+
+ public DeleteAndInsertBuilder(AbstractUpdateCallback updateCallback, Table table) {
+ super(table);
+ assert updateCallback.isInsertSupported();
+ assert updateCallback.isDeleteSupported();
+ _updateCallback = updateCallback;
+ }
+
+ @Override
+ public void execute() throws MetaModelException {
+ // retain rows in memory
+ List<Row> rows = getRowsToUpdate();
+
+ // delete rows
+ _updateCallback.deleteFrom(getTable()).where(getWhereItems()).execute();
+
+ // modify rows
+ rows = updateRows(rows);
+
+ // insert rows
+ for (Row row : rows) {
+ _updateCallback.insertInto(getTable()).like(row).execute();
+ }
+ }
+
+ private List<Row> updateRows(List<Row> rows) {
+ for (ListIterator<Row> it = rows.listIterator(); it.hasNext();) {
+ final Row original = (Row) it.next();
+ final Row updated = update(original);
+ it.set(updated);
+ }
+ return rows;
+ }
+
+ /**
+ * Produces an updated row out of the original
+ *
+ * @param original
+ * @return
+ */
+ private Row update(final Row original) {
+ SelectItem[] items = original.getSelectItems();
+ Object[] values = new Object[items.length];
+ for (int i = 0; i < items.length; i++) {
+ final Object value;
+ Column column = items[i].getColumn();
+ if (isSet(column)) {
+ // use update statement's value
+ value = getValues()[i];
+ } else {
+ // use original value
+ value = original.getValue(i);
+ }
+ values[i] = value;
+ }
+ return new DefaultRow(new SimpleDataSetHeader(items), values);
+ }
+
+ protected List<Row> getRowsToUpdate() {
+ final DataContext dc = _updateCallback.getDataContext();
+ final Table table = getTable();
+ final List<FilterItem> whereItems = getWhereItems();
+ final DataSet dataSet = dc.query().from(table).select(table.getColumns()).where(whereItems).execute();
+ final List<Row> rows = dataSet.toRows();
+ return rows;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/InconsistentRowFormatException.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/InconsistentRowFormatException.java b/core/src/main/java/org/apache/metamodel/InconsistentRowFormatException.java
new file mode 100644
index 0000000..b3baff9
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/InconsistentRowFormatException.java
@@ -0,0 +1,82 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eobjects.metamodel;
+
+import org.eobjects.metamodel.data.DataSet;
+import org.eobjects.metamodel.data.Row;
+
+/**
+ * Abstract exception type that represents exceptions that occur when reading a
+ * data format which contain formatting errors or inconsistencies in on or more
+ * rows.
+ *
+ * Typically {@link InconsistentRowFormatException}s are thrown when calling
+ * {@link DataSet#next()}.
+ *
+ * All {@link InconsistentRowFormatException}s are optional, meaning that you
+ * can turn them on and off. When turned off the result of
+ * {@link #getProposedRow()} will be used transparently instead of throwing the
+ * exception.
+ *
+ * @author Kasper Sørensen
+ */
+public abstract class InconsistentRowFormatException extends MetaModelException {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Row _proposedRow;
+ private final int _rowNumber;
+
+ public InconsistentRowFormatException(Row proposedRow, int rowNumber) {
+ super();
+ _proposedRow = proposedRow;
+ _rowNumber = rowNumber;
+ }
+
+ public InconsistentRowFormatException(Row proposedRow, int rowNumber,
+ Exception cause) {
+ super(cause);
+ _proposedRow = proposedRow;
+ _rowNumber = rowNumber;
+ }
+
+ /**
+ * Gets the row as MetaModel would gracefully interpret it.
+ *
+ * @return a row object which represents the {@link Row} as MetaModel would
+ * gracefully interpret it.
+ */
+ public Row getProposedRow() {
+ return _proposedRow;
+ }
+
+ /**
+ * Gets the row number (1-based).
+ *
+ * @return the index of the row.
+ */
+ public int getRowNumber() {
+ return _rowNumber;
+ }
+
+ @Override
+ public String getMessage() {
+ return "Inconsistent row format of row no. " + getRowNumber() + ".";
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/MetaModelException.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/MetaModelException.java b/core/src/main/java/org/apache/metamodel/MetaModelException.java
new file mode 100644
index 0000000..787b8c5
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/MetaModelException.java
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eobjects.metamodel;
+
+/**
+ * Unchecked exception used to signal errors occuring in MetaModel.
+ *
+ * All MetaModelExceptions represent errors discovered withing the MetaModel
+ * framework. Typically these will occur if you have put together a query that
+ * is not meaningful or if there is a structural problem in a schema.
+ */
+public class MetaModelException extends RuntimeException {
+
+ private static final long serialVersionUID = 5455738384633428319L;
+
+ public MetaModelException(String message, Exception cause) {
+ super(message, cause);
+ }
+
+ public MetaModelException(String message) {
+ super(message);
+ }
+
+ public MetaModelException(Exception cause) {
+ super(cause);
+ }
+
+ public MetaModelException() {
+ super();
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/MetaModelHelper.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/MetaModelHelper.java b/core/src/main/java/org/apache/metamodel/MetaModelHelper.java
new file mode 100644
index 0000000..a105973
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/MetaModelHelper.java
@@ -0,0 +1,775 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eobjects.metamodel;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.eobjects.metamodel.data.CachingDataSetHeader;
+import org.eobjects.metamodel.data.DataSet;
+import org.eobjects.metamodel.data.DataSetHeader;
+import org.eobjects.metamodel.data.DefaultRow;
+import org.eobjects.metamodel.data.EmptyDataSet;
+import org.eobjects.metamodel.data.FilteredDataSet;
+import org.eobjects.metamodel.data.IRowFilter;
+import org.eobjects.metamodel.data.InMemoryDataSet;
+import org.eobjects.metamodel.data.Row;
+import org.eobjects.metamodel.data.SimpleDataSetHeader;
+import org.eobjects.metamodel.data.SubSelectionDataSet;
+import org.eobjects.metamodel.query.FilterItem;
+import org.eobjects.metamodel.query.FromItem;
+import org.eobjects.metamodel.query.GroupByItem;
+import org.eobjects.metamodel.query.OrderByItem;
+import org.eobjects.metamodel.query.Query;
+import org.eobjects.metamodel.query.SelectItem;
+import org.eobjects.metamodel.query.parser.QueryParser;
+import org.eobjects.metamodel.schema.Column;
+import org.eobjects.metamodel.schema.ColumnType;
+import org.eobjects.metamodel.schema.Schema;
+import org.eobjects.metamodel.schema.SuperColumnType;
+import org.eobjects.metamodel.schema.Table;
+import org.eobjects.metamodel.util.AggregateBuilder;
+import org.eobjects.metamodel.util.CollectionUtils;
+import org.eobjects.metamodel.util.EqualsBuilder;
+import org.eobjects.metamodel.util.Func;
+import org.eobjects.metamodel.util.ObjectComparator;
+import org.eobjects.metamodel.util.Predicate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class contains various helper functionality to common tasks in
+ * MetaModel, eg.:
+ *
+ * <ul>
+ * <li>Easy-access for traversing common schema items</li>
+ * <li>Manipulate data in memory. These methods are primarily used to enable
+ * queries for non-queryable data sources like CSV files and spreadsheets.</li>
+ * <li>Query rewriting, traversing and manipulation.</li>
+ * </ul>
+ *
+ * The class is mainly intended for internal use within the framework
+ * operations, but is kept stable, so it can also be used by framework users.
+ */
+public final class MetaModelHelper {
+
+ private final static Logger logger = LoggerFactory.getLogger(MetaModelHelper.class);
+
+ private MetaModelHelper() {
+ // Prevent instantiation
+ }
+
+ /**
+ * Creates an array of tables where all occurences of tables in the provided
+ * list of tables and columns are included
+ */
+ public static Table[] getTables(Collection<Table> tableList, Iterable<Column> columnList) {
+ HashSet<Table> set = new HashSet<Table>();
+ set.addAll(tableList);
+ for (Column column : columnList) {
+ set.add(column.getTable());
+ }
+ return set.toArray(new Table[set.size()]);
+ }
+
+ /**
+ * Determines if a schema is an information schema
+ *
+ * @param schema
+ * @return
+ */
+ public static boolean isInformationSchema(Schema schema) {
+ String name = schema.getName();
+ return isInformationSchema(name);
+ }
+
+ /**
+ * Determines if a schema name is the name of an information schema
+ *
+ * @param name
+ * @return
+ */
+ public static boolean isInformationSchema(String name) {
+ if (name == null) {
+ return false;
+ }
+ return QueryPostprocessDataContext.INFORMATION_SCHEMA_NAME.equals(name.toLowerCase());
+ }
+
+ /**
+ * Converts a list of columns to a corresponding array of tables
+ *
+ * @param columns
+ * the columns that the tables will be extracted from
+ * @return an array containing the tables of the provided columns.
+ */
+ public static Table[] getTables(Iterable<Column> columns) {
+ ArrayList<Table> result = new ArrayList<Table>();
+ for (Column column : columns) {
+ Table table = column.getTable();
+ if (!result.contains(table)) {
+ result.add(table);
+ }
+ }
+ return result.toArray(new Table[result.size()]);
+ }
+
+ /**
+ * Creates a subset array of columns, where only columns that are contained
+ * within the specified table are included.
+ *
+ * @param table
+ * @param columns
+ * @return an array containing the columns that exist in the table
+ */
+ public static Column[] getTableColumns(Table table, Iterable<Column> columns) {
+ if (table == null) {
+ return new Column[0];
+ }
+ final List<Column> result = new ArrayList<Column>();
+ for (Column column : columns) {
+ final boolean sameTable = table.equals(column.getTable());
+ if (sameTable) {
+ result.add(column);
+ }
+ }
+ return result.toArray(new Column[result.size()]);
+ }
+
+ /**
+ * Creates a subset array of columns, where only columns that are contained
+ * within the specified table are included.
+ *
+ * @param table
+ * @param columns
+ * @return an array containing the columns that exist in the table
+ */
+ public static Column[] getTableColumns(Table table, Column[] columns) {
+ return getTableColumns(table, Arrays.asList(columns));
+ }
+
+ public static DataSet getCarthesianProduct(DataSet... fromDataSets) {
+ return getCarthesianProduct(fromDataSets, new FilterItem[0]);
+ }
+
+ public static DataSet getCarthesianProduct(DataSet[] fromDataSets, Iterable<FilterItem> whereItems) {
+ // First check if carthesian product is even nescesary
+ if (fromDataSets.length == 1) {
+ return getFiltered(fromDataSets[0], whereItems);
+ }
+
+ List<SelectItem> selectItems = new ArrayList<SelectItem>();
+ for (DataSet dataSet : fromDataSets) {
+ for (int i = 0; i < dataSet.getSelectItems().length; i++) {
+ SelectItem item = dataSet.getSelectItems()[i];
+ selectItems.add(item);
+ }
+ }
+
+ int selectItemOffset = 0;
+ List<Object[]> data = new ArrayList<Object[]>();
+ for (int fromDataSetIndex = 0; fromDataSetIndex < fromDataSets.length; fromDataSetIndex++) {
+ DataSet fromDataSet = fromDataSets[fromDataSetIndex];
+ SelectItem[] fromSelectItems = fromDataSet.getSelectItems();
+ if (fromDataSetIndex == 0) {
+ while (fromDataSet.next()) {
+ Object[] values = fromDataSet.getRow().getValues();
+ Object[] row = new Object[selectItems.size()];
+ System.arraycopy(values, 0, row, selectItemOffset, values.length);
+ data.add(row);
+ }
+ fromDataSet.close();
+ } else {
+ List<Object[]> fromDataRows = new ArrayList<Object[]>();
+ while (fromDataSet.next()) {
+ fromDataRows.add(fromDataSet.getRow().getValues());
+ }
+ fromDataSet.close();
+ for (int i = 0; i < data.size(); i = i + fromDataRows.size()) {
+ Object[] originalRow = data.get(i);
+ data.remove(i);
+ for (int j = 0; j < fromDataRows.size(); j++) {
+ Object[] newRow = fromDataRows.get(j);
+ System.arraycopy(newRow, 0, originalRow, selectItemOffset, newRow.length);
+ data.add(i + j, originalRow.clone());
+ }
+ }
+ }
+ selectItemOffset += fromSelectItems.length;
+ }
+
+ if (data.isEmpty()) {
+ return new EmptyDataSet(selectItems);
+ }
+
+ final DataSetHeader header = new CachingDataSetHeader(selectItems);
+ final List<Row> rows = new ArrayList<Row>(data.size());
+ for (Object[] objects : data) {
+ rows.add(new DefaultRow(header, objects, null));
+ }
+
+ DataSet result = new InMemoryDataSet(header, rows);
+ if (whereItems != null) {
+ result = getFiltered(result, whereItems);
+ }
+ return result;
+ }
+
+ public static DataSet getCarthesianProduct(DataSet[] fromDataSets, FilterItem... filterItems) {
+ return getCarthesianProduct(fromDataSets, Arrays.asList(filterItems));
+ }
+
+ public static DataSet getFiltered(DataSet dataSet, Iterable<FilterItem> filterItems) {
+ List<IRowFilter> filters = CollectionUtils.map(filterItems, new Func<FilterItem, IRowFilter>() {
+ @Override
+ public IRowFilter eval(FilterItem filterItem) {
+ return filterItem;
+ }
+ });
+ if (filters.isEmpty()) {
+ return dataSet;
+ }
+
+ return new FilteredDataSet(dataSet, filters.toArray(new IRowFilter[filters.size()]));
+ }
+
+ public static DataSet getFiltered(DataSet dataSet, FilterItem... filterItems) {
+ return getFiltered(dataSet, Arrays.asList(filterItems));
+ }
+
+ public static DataSet getSelection(final List<SelectItem> selectItems, final DataSet dataSet) {
+ final SelectItem[] dataSetSelectItems = dataSet.getSelectItems();
+
+ // check if the selection is already the same
+ if (selectItems.size() == dataSetSelectItems.length) {
+ boolean same = true;
+ int i = 0;
+ for (SelectItem selectItem : selectItems) {
+ if (!EqualsBuilder.equals(selectItem, dataSetSelectItems[i])) {
+ same = false;
+ break;
+ }
+ i++;
+ }
+
+ if (same) {
+ // return the dataSet unmodified
+ return dataSet;
+ }
+ }
+
+ SelectItem[] selectItemsArray = selectItems.toArray(new SelectItem[selectItems.size()]);
+ return new SubSelectionDataSet(selectItemsArray, dataSet);
+ }
+
+ public static DataSet getSelection(SelectItem[] selectItems, DataSet dataSet) {
+ return getSelection(Arrays.asList(selectItems), dataSet);
+ }
+
+ public static DataSet getGrouped(List<SelectItem> selectItems, DataSet dataSet, Collection<GroupByItem> groupByItems) {
+ return getGrouped(selectItems, dataSet, groupByItems.toArray(new GroupByItem[groupByItems.size()]));
+ }
+
+ public static DataSet getGrouped(List<SelectItem> selectItems, DataSet dataSet, GroupByItem[] groupByItems) {
+ DataSet result = dataSet;
+ if (groupByItems != null && groupByItems.length > 0) {
+ Map<Row, Map<SelectItem, List<Object>>> uniqueRows = new HashMap<Row, Map<SelectItem, List<Object>>>();
+
+ final SelectItem[] groupBySelects = new SelectItem[groupByItems.length];
+ for (int i = 0; i < groupBySelects.length; i++) {
+ groupBySelects[i] = groupByItems[i].getSelectItem();
+ }
+ final DataSetHeader groupByHeader = new CachingDataSetHeader(groupBySelects);
+
+ // Creates a list of SelectItems that have functions
+ List<SelectItem> functionItems = getFunctionSelectItems(selectItems);
+
+ // Loop through the dataset and identify groups
+ while (dataSet.next()) {
+ Row row = dataSet.getRow();
+
+ // Subselect a row prototype with only the unique values that
+ // define the group
+ Row uniqueRow = row.getSubSelection(groupByHeader);
+
+ // function input is the values used for calculating aggregate
+ // functions in the group
+ Map<SelectItem, List<Object>> functionInput;
+ if (!uniqueRows.containsKey(uniqueRow)) {
+ // If this group already exist, use an existing function
+ // input
+ functionInput = new HashMap<SelectItem, List<Object>>();
+ for (SelectItem item : functionItems) {
+ functionInput.put(item, new ArrayList<Object>());
+ }
+ uniqueRows.put(uniqueRow, functionInput);
+ } else {
+ // If this is a new group, create a new function input
+ functionInput = uniqueRows.get(uniqueRow);
+ }
+
+ // Loop through aggregate functions to check for validity
+ for (SelectItem item : functionItems) {
+ List<Object> objects = functionInput.get(item);
+ Column column = item.getColumn();
+ if (column != null) {
+ Object value = row.getValue(new SelectItem(column));
+ objects.add(value);
+ } else if (SelectItem.isCountAllItem(item)) {
+ // Just use the empty string, since COUNT(*) don't
+ // evaluate values (but null values should be prevented)
+ objects.add("");
+ } else {
+ throw new IllegalArgumentException("Expression function not supported: " + item);
+ }
+ }
+ }
+
+ dataSet.close();
+ final List<Row> resultData = new ArrayList<Row>();
+ final DataSetHeader resultHeader = new CachingDataSetHeader(selectItems);
+
+ // Loop through the groups to generate aggregates
+ for (Entry<Row, Map<SelectItem, List<Object>>> entry : uniqueRows.entrySet()) {
+ Row row = entry.getKey();
+ Map<SelectItem, List<Object>> functionInput = entry.getValue();
+ Object[] resultRow = new Object[selectItems.size()];
+ // Loop through select items to generate a row
+ int i = 0;
+ for (SelectItem item : selectItems) {
+ int uniqueRowIndex = row.indexOf(item);
+ if (uniqueRowIndex != -1) {
+ // If there's already a value for the select item in the
+ // row, keep it (it's one of the grouped by columns)
+ resultRow[i] = row.getValue(uniqueRowIndex);
+ } else {
+ // Use the function input to calculate the aggregate
+ // value
+ List<Object> objects = functionInput.get(item);
+ if (objects != null) {
+ Object functionResult = item.getFunction().evaluate(objects.toArray());
+ resultRow[i] = functionResult;
+ } else {
+ if (item.getFunction() != null) {
+ logger.error("No function input found for SelectItem: {}", item);
+ }
+ }
+ }
+ i++;
+ }
+ resultData.add(new DefaultRow(resultHeader, resultRow, null));
+ }
+
+ if (resultData.isEmpty()) {
+ result = new EmptyDataSet(selectItems);
+ } else {
+ result = new InMemoryDataSet(resultHeader, resultData);
+ }
+ }
+ result = getSelection(selectItems, result);
+ return result;
+ }
+
+ /**
+ * Applies aggregate values to a dataset. This method is to be invoked AFTER
+ * any filters have been applied.
+ *
+ * @param workSelectItems
+ * all select items included in the processing of the query
+ * (including those originating from other clauses than the
+ * SELECT clause).
+ * @param dataSet
+ * @return
+ */
+ public static DataSet getAggregated(List<SelectItem> workSelectItems, DataSet dataSet) {
+ final List<SelectItem> functionItems = getFunctionSelectItems(workSelectItems);
+ if (functionItems.isEmpty()) {
+ return dataSet;
+ }
+
+ final Map<SelectItem, AggregateBuilder<?>> aggregateBuilders = new HashMap<SelectItem, AggregateBuilder<?>>();
+ for (SelectItem item : functionItems) {
+ aggregateBuilders.put(item, item.getFunction().build());
+ }
+
+ final DataSetHeader header;
+ final boolean onlyAggregates;
+ if (functionItems.size() != workSelectItems.size()) {
+ onlyAggregates = false;
+ header = new CachingDataSetHeader(workSelectItems);
+ } else {
+ onlyAggregates = true;
+ header = new SimpleDataSetHeader(workSelectItems);
+ }
+
+ final List<Row> resultRows = new ArrayList<Row>();
+ while (dataSet.next()) {
+ final Row inputRow = dataSet.getRow();
+ for (SelectItem item : functionItems) {
+ final AggregateBuilder<?> aggregateBuilder = aggregateBuilders.get(item);
+ final Column column = item.getColumn();
+ if (column != null) {
+ Object value = inputRow.getValue(new SelectItem(column));
+ aggregateBuilder.add(value);
+ } else if (SelectItem.isCountAllItem(item)) {
+ // Just use the empty string, since COUNT(*) don't
+ // evaluate values (but null values should be prevented)
+ aggregateBuilder.add("");
+ } else {
+ throw new IllegalArgumentException("Expression function not supported: " + item);
+ }
+ }
+
+ // If the result should also contain non-aggregated values, we
+ // will keep those in the rows list
+ if (!onlyAggregates) {
+ final Object[] values = new Object[header.size()];
+ for (int i = 0; i < header.size(); i++) {
+ final Object value = inputRow.getValue(header.getSelectItem(i));
+ if (value != null) {
+ values[i] = value;
+ }
+ }
+ resultRows.add(new DefaultRow(header, values));
+ }
+ }
+ dataSet.close();
+
+ // Collect the aggregates
+ Map<SelectItem, Object> functionResult = new HashMap<SelectItem, Object>();
+ for (SelectItem item : functionItems) {
+ AggregateBuilder<?> aggregateBuilder = aggregateBuilders.get(item);
+ Object result = aggregateBuilder.getAggregate();
+ functionResult.put(item, result);
+ }
+
+ // if there are no result rows (no matching records at all), we still
+ // need to return a record with the aggregates
+ final boolean noResultRows = resultRows.isEmpty();
+
+ if (onlyAggregates || noResultRows) {
+ // We will only create a single row with all the aggregates
+ Object[] values = new Object[header.size()];
+ for (int i = 0; i < header.size(); i++) {
+ values[i] = functionResult.get(header.getSelectItem(i));
+ }
+ Row row = new DefaultRow(header, values);
+ resultRows.add(row);
+ } else {
+ // We will create the aggregates as well as regular values
+ for (int i = 0; i < resultRows.size(); i++) {
+ Row row = resultRows.get(i);
+ Object[] values = row.getValues();
+ for (Entry<SelectItem, Object> entry : functionResult.entrySet()) {
+ SelectItem item = entry.getKey();
+ int itemIndex = row.indexOf(item);
+ if (itemIndex != -1) {
+ Object value = entry.getValue();
+ values[itemIndex] = value;
+ }
+ }
+ resultRows.set(i, new DefaultRow(header, values));
+ }
+ }
+
+ return new InMemoryDataSet(header, resultRows);
+ }
+
+ public static List<SelectItem> getFunctionSelectItems(Iterable<SelectItem> selectItems) {
+ return CollectionUtils.filter(selectItems, new Predicate<SelectItem>() {
+ @Override
+ public Boolean eval(SelectItem arg) {
+ return arg.getFunction() != null;
+ }
+ });
+ }
+
+ public static DataSet getOrdered(DataSet dataSet, List<OrderByItem> orderByItems) {
+ return getOrdered(dataSet, orderByItems.toArray(new OrderByItem[orderByItems.size()]));
+ }
+
+ public static DataSet getOrdered(DataSet dataSet, final OrderByItem... orderByItems) {
+ if (orderByItems != null && orderByItems.length != 0) {
+ final int[] sortIndexes = new int[orderByItems.length];
+ for (int i = 0; i < orderByItems.length; i++) {
+ OrderByItem item = orderByItems[i];
+ int indexOf = dataSet.indexOf(item.getSelectItem());
+ sortIndexes[i] = indexOf;
+ }
+
+ final List<Row> data = readDataSetFull(dataSet);
+ if (data.isEmpty()) {
+ return new EmptyDataSet(dataSet.getSelectItems());
+ }
+
+ final Comparator<Object> valueComparator = ObjectComparator.getComparator();
+
+ // create a comparator for doing the actual sorting/ordering
+ final Comparator<Row> comparator = new Comparator<Row>() {
+ public int compare(Row o1, Row o2) {
+ for (int i = 0; i < sortIndexes.length; i++) {
+ int sortIndex = sortIndexes[i];
+ Object sortObj1 = o1.getValue(sortIndex);
+ Object sortObj2 = o2.getValue(sortIndex);
+ int compare = valueComparator.compare(sortObj1, sortObj2);
+ if (compare != 0) {
+ OrderByItem orderByItem = orderByItems[i];
+ boolean ascending = orderByItem.isAscending();
+ if (ascending) {
+ return compare;
+ } else {
+ return compare * -1;
+ }
+ }
+ }
+ return 0;
+ }
+ };
+
+ Collections.sort(data, comparator);
+
+ dataSet = new InMemoryDataSet(data);
+ }
+ return dataSet;
+ }
+
+ public static List<Row> readDataSetFull(DataSet dataSet) {
+ final List<Row> result;
+ if (dataSet instanceof InMemoryDataSet) {
+ // if dataset is an in memory dataset we have a shortcut to avoid
+ // creating a new list
+ result = ((InMemoryDataSet) dataSet).getRows();
+ } else {
+ result = new ArrayList<Row>();
+ while (dataSet.next()) {
+ result.add(dataSet.getRow());
+ }
+ }
+ dataSet.close();
+ return result;
+ }
+
+ /**
+ * Examines a query and extracts an array of FromItem's that refer
+ * (directly) to tables (hence Joined FromItems and SubQuery FromItems are
+ * traversed but not included).
+ *
+ * @param q
+ * the query to examine
+ * @return an array of FromItem's that refer directly to tables
+ */
+ public static FromItem[] getTableFromItems(Query q) {
+ List<FromItem> result = new ArrayList<FromItem>();
+ List<FromItem> items = q.getFromClause().getItems();
+ for (FromItem item : items) {
+ result.addAll(getTableFromItems(item));
+ }
+ return result.toArray(new FromItem[result.size()]);
+ }
+
+ public static List<FromItem> getTableFromItems(FromItem item) {
+ List<FromItem> result = new ArrayList<FromItem>();
+ if (item.getTable() != null) {
+ result.add(item);
+ } else if (item.getSubQuery() != null) {
+ FromItem[] sqItems = getTableFromItems(item.getSubQuery());
+ for (int i = 0; i < sqItems.length; i++) {
+ result.add(sqItems[i]);
+ }
+ } else if (item.getJoin() != null) {
+ FromItem leftSide = item.getLeftSide();
+ result.addAll(getTableFromItems(leftSide));
+ FromItem rightSide = item.getRightSide();
+ result.addAll(getTableFromItems(rightSide));
+ } else {
+ throw new IllegalStateException("FromItem was neither of Table type, SubQuery type or Join type: " + item);
+ }
+ return result;
+ }
+
+ /**
+ * Executes a single row query, like "SELECT COUNT(*), MAX(SOME_COLUMN) FROM
+ * MY_TABLE" or similar.
+ *
+ * @param dataContext
+ * the DataContext object to use for executing the query
+ * @param query
+ * the query to execute
+ * @return a row object representing the single row returned from the query
+ * @throws MetaModelException
+ * if less or more than one Row is returned from the query
+ */
+ public static Row executeSingleRowQuery(DataContext dataContext, Query query) throws MetaModelException {
+ DataSet dataSet = dataContext.executeQuery(query);
+ boolean next = dataSet.next();
+ if (!next) {
+ throw new MetaModelException("No rows returned from query: " + query);
+ }
+ Row row = dataSet.getRow();
+ next = dataSet.next();
+ if (next) {
+ throw new MetaModelException("More than one row returned from query: " + query);
+ }
+ dataSet.close();
+ return row;
+ }
+
+ /**
+ * Performs a left join (aka left outer join) operation on two datasets.
+ *
+ * @param ds1
+ * the left dataset
+ * @param ds2
+ * the right dataset
+ * @param onConditions
+ * the conditions to join by
+ * @return the left joined result dataset
+ */
+ public static DataSet getLeftJoin(DataSet ds1, DataSet ds2, FilterItem[] onConditions) {
+ if (ds1 == null) {
+ throw new IllegalArgumentException("Left DataSet cannot be null");
+ }
+ if (ds2 == null) {
+ throw new IllegalArgumentException("Right DataSet cannot be null");
+ }
+ SelectItem[] si1 = ds1.getSelectItems();
+ SelectItem[] si2 = ds2.getSelectItems();
+ SelectItem[] selectItems = new SelectItem[si1.length + si2.length];
+ System.arraycopy(si1, 0, selectItems, 0, si1.length);
+ System.arraycopy(si2, 0, selectItems, si1.length, si2.length);
+
+ List<Row> resultRows = new ArrayList<Row>();
+ List<Row> ds2data = readDataSetFull(ds2);
+ if (ds2data.isEmpty()) {
+ // no need to join, simply return a new view (with null values) on
+ // the previous dataset.
+ return getSelection(selectItems, ds1);
+ }
+
+ final DataSetHeader header = new CachingDataSetHeader(selectItems);
+
+ while (ds1.next()) {
+
+ // Construct a single-row dataset for making a carthesian product
+ // against ds2
+ Row ds1row = ds1.getRow();
+ List<Row> ds1rows = new ArrayList<Row>();
+ ds1rows.add(ds1row);
+
+ DataSet carthesianProduct = getCarthesianProduct(new DataSet[] {
+ new InMemoryDataSet(new CachingDataSetHeader(si1), ds1rows),
+ new InMemoryDataSet(new CachingDataSetHeader(si2), ds2data) }, onConditions);
+ List<Row> carthesianRows = readDataSetFull(carthesianProduct);
+ if (carthesianRows.size() > 0) {
+ resultRows.addAll(carthesianRows);
+ } else {
+ Object[] values = ds1row.getValues();
+ Object[] row = new Object[selectItems.length];
+ System.arraycopy(values, 0, row, 0, values.length);
+ resultRows.add(new DefaultRow(header, row));
+ }
+ }
+ ds1.close();
+
+ if (resultRows.isEmpty()) {
+ return new EmptyDataSet(selectItems);
+ }
+
+ return new InMemoryDataSet(header, resultRows);
+ }
+
+ /**
+ * Performs a right join (aka right outer join) operation on two datasets.
+ *
+ * @param ds1
+ * the left dataset
+ * @param ds2
+ * the right dataset
+ * @param onConditions
+ * the conditions to join by
+ * @return the right joined result dataset
+ */
+ public static DataSet getRightJoin(DataSet ds1, DataSet ds2, FilterItem[] onConditions) {
+ SelectItem[] ds1selects = ds1.getSelectItems();
+ SelectItem[] ds2selects = ds2.getSelectItems();
+ SelectItem[] leftOrderedSelects = new SelectItem[ds1selects.length + ds2selects.length];
+ System.arraycopy(ds1selects, 0, leftOrderedSelects, 0, ds1selects.length);
+ System.arraycopy(ds2selects, 0, leftOrderedSelects, ds1selects.length, ds2selects.length);
+
+ // We will reuse the left join algorithm (but switch the datasets
+ // around)
+ DataSet dataSet = getLeftJoin(ds2, ds1, onConditions);
+
+ dataSet = getSelection(leftOrderedSelects, dataSet);
+ return dataSet;
+ }
+
+ public static SelectItem[] createSelectItems(Column... columns) {
+ SelectItem[] items = new SelectItem[columns.length];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = new SelectItem(columns[i]);
+ }
+ return items;
+ }
+
+ public static DataSet getDistinct(DataSet dataSet) {
+ SelectItem[] selectItems = dataSet.getSelectItems();
+ GroupByItem[] groupByItems = new GroupByItem[selectItems.length];
+ for (int i = 0; i < groupByItems.length; i++) {
+ groupByItems[i] = new GroupByItem(selectItems[i]);
+ }
+ return getGrouped(Arrays.asList(selectItems), dataSet, groupByItems);
+ }
+
+ public static Table[] getTables(Column[] columns) {
+ return getTables(Arrays.asList(columns));
+ }
+
+ public static Column[] getColumnsByType(Column[] columns, final ColumnType columnType) {
+ return CollectionUtils.filter(columns, new Predicate<Column>() {
+ @Override
+ public Boolean eval(Column column) {
+ return column.getType() == columnType;
+ }
+ }).toArray(new Column[0]);
+ }
+
+ public static Column[] getColumnsBySuperType(Column[] columns, final SuperColumnType superColumnType) {
+ return CollectionUtils.filter(columns, new Predicate<Column>() {
+ @Override
+ public Boolean eval(Column column) {
+ return column.getType().getSuperType() == superColumnType;
+ }
+ }).toArray(new Column[0]);
+ }
+
+ public static Query parseQuery(DataContext dc, String queryString) {
+ final QueryParser parser = new QueryParser(dc, queryString);
+ return parser.parse();
+ }
+
+}
\ No newline at end of file