[CALCITE-884] Web adapter (Henry Olson)

Some fix-ups (Julian Hyde):
* Upgrade calcite;
* Upgrade natty, jsoup;
* Rename files, and move to org.apache.calcite.adapter.file package;
* Add file adapter documentation and history (based on original README
  and HISTORY);
* Add EMPS.html and DEPTS.html examples.


Project: http://git-wip-us.apache.org/repos/asf/calcite/repo
Commit: http://git-wip-us.apache.org/repos/asf/calcite/commit/cf46d3be
Tree: http://git-wip-us.apache.org/repos/asf/calcite/tree/cf46d3be
Diff: http://git-wip-us.apache.org/repos/asf/calcite/diff/cf46d3be

Branch: refs/heads/master
Commit: cf46d3be19f701ad056aba3b60a701b962e0f1b0
Parents: 1b11385
Author: Henry Olson <[email protected]>
Authored: Thu Apr 14 22:31:10 2016 +0100
Committer: Julian Hyde <[email protected]>
Committed: Wed Feb 22 21:09:47 2017 -0800

----------------------------------------------------------------------
 .../apache/calcite/schema/SchemaFactory.java    |  34 +-
 file/pom.xml                                    | 112 ++++++
 .../calcite/adapter/file/FileEnumerator.java    |  94 +++++
 .../calcite/adapter/file/FileFieldType.java     |  86 ++++
 .../apache/calcite/adapter/file/FileReader.java | 232 +++++++++++
 .../adapter/file/FileReaderException.java       |  31 ++
 .../calcite/adapter/file/FileRowConverter.java  | 393 +++++++++++++++++++
 .../apache/calcite/adapter/file/FileSchema.java |  67 ++++
 .../calcite/adapter/file/FileSchemaFactory.java |  47 +++
 .../apache/calcite/adapter/file/FileTable.java  | 122 ++++++
 .../calcite/adapter/file/FileTableScan.java     |  94 +++++
 .../calcite/adapter/file/package-info.java      |  27 ++
 .../calcite/adapter/file/FileReaderTest.java    | 185 +++++++++
 .../apache/calcite/adapter/file/FileSuite.java  |  58 +++
 .../apache/calcite/adapter/file/SqlTest.java    | 189 +++++++++
 .../calcite/adapter/file/package-info.java      |  27 ++
 file/src/test/resources/sales.json              |  18 +
 file/src/test/resources/sales/DEPTS.html        |  42 ++
 file/src/test/resources/sales/EMPS.html         |  56 +++
 file/src/test/resources/tableNoTH.html          |  39 ++
 file/src/test/resources/tableNoTheadTbody.html  |  44 +++
 file/src/test/resources/tableOK.html            |  30 ++
 file/src/test/resources/tableX2.html            |  69 ++++
 file/src/test/resources/testModel.json          |  21 +
 file/src/test/resources/webjoin.sql             |  25 ++
 file/src/test/resources/wiki.json               |  93 +++++
 pom.xml                                         |  18 +
 site/_docs/adapter.md                           |   1 +
 site/_docs/file.md                              | 236 +++++++++++
 site/_docs/history.md                           |  15 +
 sqlline                                         |   2 +-
 sqlline.bat                                     |   2 +-
 32 files changed, 2490 insertions(+), 19 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/core/src/main/java/org/apache/calcite/schema/SchemaFactory.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/schema/SchemaFactory.java 
b/core/src/main/java/org/apache/calcite/schema/SchemaFactory.java
index 57f5b4b..89f8496 100644
--- a/core/src/main/java/org/apache/calcite/schema/SchemaFactory.java
+++ b/core/src/main/java/org/apache/calcite/schema/SchemaFactory.java
@@ -24,27 +24,27 @@ import java.util.Map;
  * <p>A schema factory allows you to include a custom schema in a model file.
  * For example, here is a model that contains a custom schema whose tables
  * read CSV files. (See the
- * <a href="https://github.com/julianhyde/optiq-csv";>optiq-csv</a> for more
- * details about this particular adapter.)</p>
+ * <a 
href="http://calcite.apache.org/apidocs/org/apache/calcite/adapter/csv/package-summary.html";>example
 CSV adapter</a>
+ * for more details about this particular adapter.)
  *
  * <pre>{@code
  * {
- *   version: '1.0',
- *   defaultSchema: 'SALES',
- *   schemas: [
+ *   "version": "1.0",
+ *   "defaultSchema": "SALES",
+ *   "schemas": [
  *     {
- *       name: 'SALES',
- *       type: 'custom',
- *       factory: 'org.apache.calcite.adapter.csv.CsvSchemaFactory',
- *       mutable: true,
- *       operand: {
- *         directory: 'target/test-classes/sales'
+ *       "name": "SALES",
+ *       "type": "custom",
+ *       "factory": "org.apache.calcite.adapter.csv.CsvSchemaFactory",
+ *       "mutable": true,
+ *       "operand": {
+ *         directory: "target/test-classes/sales"
  *       },
- *       tables: [
+ *       "tables": [
  *         {
- *           name: 'FEMALE_EMPS',
- *           type: 'view',
- *           sql: 'SELECT * FROM emps WHERE gender = \'F\''
+ *           "name": "FEMALE_EMPS",
+ *           "type": "view",
+ *           "sql": "SELECT * FROM emps WHERE gender = 'F'"
  *          }
  *       ]
  *     }
@@ -55,10 +55,10 @@ import java.util.Map;
  *
  * <p>If you do not wish to allow model authors to add additional tables
  * (including views) to an instance of your schema, specify
- * 'mutable: false'.</p>
+ * 'mutable: false'.
  *
  * <p>A class that implements SchemaFactory specified in a schema must have a
- * public default constructor.</p>
+ * public default constructor.
  */
 public interface SchemaFactory {
   /** Creates a Schema.

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/pom.xml
----------------------------------------------------------------------
diff --git a/file/pom.xml b/file/pom.xml
new file mode 100644
index 0000000..d8173a5
--- /dev/null
+++ b/file/pom.xml
@@ -0,0 +1,112 @@
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.calcite</groupId>
+    <artifactId>calcite</artifactId>
+    <version>1.12.0-SNAPSHOT</version>
+  </parent>
+
+  <!-- The basics. -->
+  <artifactId>calcite-file</artifactId>
+  <packaging>jar</packaging>
+  <version>1.12.0-SNAPSHOT</version>
+  <name>Calcite File</name>
+  <description>Calcite provider that reads files and URIs</description>
+
+  <properties>
+    <top.dir>${project.basedir}/..</top.dir>
+    <build.timestamp>${maven.build.timestamp}</build.timestamp>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.joestelmach</groupId>
+      <artifactId>natty</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.calcite</groupId>
+      <artifactId>calcite-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.calcite</groupId>
+      <artifactId>calcite-linq4j</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jsoup</groupId>
+      <artifactId>jsoup</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+          <groupId>org.codehaus.mojo</groupId>
+             <artifactId>jalopy-maven-plugin</artifactId>
+             <version>1.0-alpha-1</version>
+             <configuration>
+               <srcExcludesPattern>**/CsvTableScan.java</srcExcludesPattern>
+            <failOnError>false</failOnError>
+            <convention>${top.dir}src/main/config/jalopy.xml</convention>
+             </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <version>2.18</version>
+        <configuration>
+          <includes>
+            <include>org/apache/calcite/adapter/file/FileSuite.java</include>
+          </includes>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <version>2.8</version>
+        <executions>
+          <execution>
+            <id>copy-dependencies</id>
+            <phase>package</phase>
+            <goals>
+              <goal>copy-dependencies</goal>
+            </goals>
+            <configuration>
+              
<outputDirectory>${project.build.directory}/dependencies/</outputDirectory>
+              <overWriteReleases>false</overWriteReleases>
+              <overWriteSnapshots>false</overWriteSnapshots>
+              <overWriteIfNewer>true</overWriteIfNewer>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/main/java/org/apache/calcite/adapter/file/FileEnumerator.java
----------------------------------------------------------------------
diff --git 
a/file/src/main/java/org/apache/calcite/adapter/file/FileEnumerator.java 
b/file/src/main/java/org/apache/calcite/adapter/file/FileEnumerator.java
new file mode 100644
index 0000000..952ccd3
--- /dev/null
+++ b/file/src/main/java/org/apache/calcite/adapter/file/FileEnumerator.java
@@ -0,0 +1,94 @@
+/*
+ * 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.apache.calcite.adapter.file;
+
+import org.apache.calcite.linq4j.Enumerator;
+
+import org.jsoup.select.Elements;
+
+import java.util.Iterator;
+
+/**
+ * Wraps WebReader and FileRowConverter, enumerates tr Elements as
+ * table rows.
+ */
+class FileEnumerator implements Enumerator<Object> {
+
+  private final Iterator<Elements> iterator;
+  private final FileRowConverter converter;
+  private final int[] fields;
+  private Object current;
+
+  public FileEnumerator(Iterator<Elements> iterator,
+      FileRowConverter converter) {
+    this.iterator = iterator;
+    this.converter = converter;
+    this.fields = identityList(this.converter.width());
+  }
+
+  public FileEnumerator(Iterator<Elements> iterator, FileRowConverter 
converter,
+      int[] fields) {
+    this.iterator = iterator;
+    this.converter = converter;
+    this.fields = fields;
+  }
+
+  public Object current() {
+    if (current == null) {
+      this.moveNext();
+    }
+    return current;
+  }
+
+  public boolean moveNext() {
+    try {
+      if (this.iterator.hasNext()) {
+        final Elements row = this.iterator.next();
+        current = this.converter.toRow(row, this.fields);
+        return true;
+      } else {
+        current = null;
+        return false;
+      }
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  // required by linq4j Enumerator interface
+  public void reset() {
+    throw new UnsupportedOperationException();
+  }
+
+  // required by linq4j Enumerator interface
+  public void close() {
+  }
+
+  /** Returns an array of integers {0, ..., n - 1}. */
+  private static int[] identityList(int n) {
+    int[] integers = new int[n];
+
+    for (int i = 0; i < n; i++) {
+      integers[i] = i;
+    }
+
+    return integers;
+  }
+
+}
+
+// End FileEnumerator.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/main/java/org/apache/calcite/adapter/file/FileFieldType.java
----------------------------------------------------------------------
diff --git 
a/file/src/main/java/org/apache/calcite/adapter/file/FileFieldType.java 
b/file/src/main/java/org/apache/calcite/adapter/file/FileFieldType.java
new file mode 100644
index 0000000..3b3c5b9
--- /dev/null
+++ b/file/src/main/java/org/apache/calcite/adapter/file/FileFieldType.java
@@ -0,0 +1,86 @@
+/*
+ * 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.apache.calcite.adapter.file;
+
+import org.apache.calcite.adapter.java.JavaTypeFactory;
+import org.apache.calcite.linq4j.tree.Primitive;
+import org.apache.calcite.rel.type.RelDataType;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+/**
+ * Type of a field in a Web (HTML) table.
+ *
+ * <p>Usually, and unless specified explicitly in the header row, a field is
+ * of type {@link #STRING}. But specifying the field type in the fields
+ * makes it easier to write SQL.
+ *
+ * <p>Trivially modified from CsvFieldType.
+ */
+enum FileFieldType {
+  STRING(null, String.class),
+  BOOLEAN(Primitive.BOOLEAN),
+  BYTE(Primitive.BYTE),
+  CHAR(Primitive.CHAR),
+  SHORT(Primitive.SHORT),
+  INT(Primitive.INT),
+  LONG(Primitive.LONG),
+  FLOAT(Primitive.FLOAT),
+  DOUBLE(Primitive.DOUBLE),
+  DATE(null, java.sql.Date.class),
+  TIME(null, java.sql.Time.class),
+  TIMESTAMP(null, java.sql.Timestamp.class);
+
+  private final Primitive primitive;
+  private final Class clazz;
+
+  private static final Map<String, FileFieldType> MAP;
+
+  static {
+    ImmutableMap.Builder<String, FileFieldType> builder =
+        ImmutableMap.builder();
+    for (FileFieldType value : values()) {
+      builder.put(value.clazz.getSimpleName(), value);
+
+      if (value.primitive != null) {
+        builder.put(value.primitive.primitiveClass.getSimpleName(), value);
+      }
+    }
+    MAP = builder.build();
+  }
+
+  FileFieldType(Primitive primitive) {
+    this(primitive, primitive.boxClass);
+  }
+
+  FileFieldType(Primitive primitive, Class clazz) {
+    this.primitive = primitive;
+    this.clazz = clazz;
+  }
+
+  public RelDataType toType(JavaTypeFactory typeFactory) {
+    return typeFactory.createJavaType(clazz);
+  }
+
+  public static FileFieldType of(String typeString) {
+    return MAP.get(typeString);
+  }
+}
+
+// End FileFieldType.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/main/java/org/apache/calcite/adapter/file/FileReader.java
----------------------------------------------------------------------
diff --git a/file/src/main/java/org/apache/calcite/adapter/file/FileReader.java 
b/file/src/main/java/org/apache/calcite/adapter/file/FileReader.java
new file mode 100644
index 0000000..bd26f4a
--- /dev/null
+++ b/file/src/main/java/org/apache/calcite/adapter/file/FileReader.java
@@ -0,0 +1,232 @@
+/*
+ * 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.apache.calcite.adapter.file;
+
+import org.jsoup.Jsoup;
+
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+
+import org.jsoup.select.Elements;
+
+import java.io.File;
+
+import java.io.IOException;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import java.util.Iterator;
+
+/**
+ * Scrapes HTML tables from URLs using Jsoup.
+ */
+public class FileReader implements Iterable<Elements> {
+
+  private static final String DEFAULT_CHARSET = "UTF-8";
+
+  private final URL url;
+  private final String selector;
+  private final Integer index;
+  private final String charset = DEFAULT_CHARSET;
+  private Element tableElement;
+  private Elements headings;
+
+  public FileReader(String url, String selector, Integer index)
+      throws FileReaderException {
+    if (url == null) {
+      throw new FileReaderException("URL must not be null");
+    }
+
+    try {
+      this.url = new URL(url);
+    } catch (MalformedURLException e) {
+      throw new FileReaderException("Malformed URL: '" + url + "'", e);
+    }
+    this.selector = selector;
+    this.index = index;
+  }
+
+  public FileReader(String url, String selector) throws FileReaderException {
+    this(url, selector, null);
+  }
+
+  public FileReader(String url) throws FileReaderException {
+    this(url, null, null);
+  }
+
+  private void getTable() throws FileReaderException {
+
+    Document doc;
+    try {
+      String proto = this.url.getProtocol();
+      if (proto.equals("file")) {
+        doc = Jsoup.parse(new File(this.url.getFile()), this.charset);
+      } else {
+        doc = Jsoup.connect(this.url.toString()).get();
+      }
+    } catch (IOException e) {
+      throw new FileReaderException("Cannot read " + this.url.toString(), e);
+    }
+
+    this.tableElement = (this.selector != null && !this.selector.equals(""))
+        ? getSelectedTable(doc, this.selector) : getBestTable(doc);
+
+  }
+
+  private Element getSelectedTable(Document doc, String selector)
+      throws FileReaderException {
+    // get selected elements
+    Elements list = doc.select(selector);
+
+    // get the element
+    Element el;
+
+    if (this.index == null) {
+      if (list.size() != 1) {
+        throw new FileReaderException("" + list.size()
+            + " HTML element(s) selected");
+      }
+
+      el = list.first();
+    } else {
+      el = list.get(this.index);
+    }
+
+    // verify element is a table
+    if (el.tag().getName().equals("table")) {
+      return el;
+    } else {
+      throw new FileReaderException("selected (" + selector + ") element is a "
+          + el.tag().getName() + ", not a table");
+    }
+  }
+
+  private Element getBestTable(Document doc) throws FileReaderException {
+    Element bestTable = null;
+    int bestScore = -1;
+
+    for (Element t : doc.select("table")) {
+      int rows = t.select("tr").size();
+      Element firstRow = t.select("tr").get(0);
+      int cols = firstRow.select("th,td").size();
+      int thisScore = rows * cols;
+      if (thisScore > bestScore) {
+        bestTable = t;
+        bestScore = thisScore;
+      }
+    }
+
+    if (bestTable == null) {
+      throw new FileReaderException("no tables found");
+    }
+
+    return bestTable;
+  }
+
+  void refresh() throws FileReaderException {
+    this.headings = null;
+    getTable();
+  }
+
+  Elements getHeadings() throws FileReaderException {
+
+    if (this.headings == null) {
+      this.iterator();
+    }
+
+    return this.headings;
+  }
+
+  private String tableKey() {
+    return "Table: {url: " + this.url + ", selector: " + this.selector;
+  }
+
+  public FileReaderIterator iterator() {
+    if (this.tableElement == null) {
+      try {
+        getTable();
+      } catch (Exception e) {
+        // TODO: temporary hack
+        throw new RuntimeException(e);
+      }
+    }
+
+    FileReaderIterator iterator =
+        new FileReaderIterator(this.tableElement.select("tr"));
+
+    // if we haven't cached the headings, get them
+    // TODO: this needs to be reworked to properly cache the headings
+    //if (this.headings == null) {
+    if (true) {
+      // first row must contain headings
+      Elements headings = iterator.next("th");
+      // if not, generate some default column names
+      if (headings.size() == 0) {
+        // rewind and peek at the first row of data
+        iterator = new FileReaderIterator(this.tableElement.select("tr"));
+        Elements firstRow = iterator.next("td");
+        int i = 0;
+        headings = new Elements();
+        for (Element td : firstRow) {
+          Element th = td.clone();
+          th.tagName("th");
+          th.html("col" + i++);
+          headings.add(th);
+        }
+        // rewind, so queries see the first row
+        iterator = new FileReaderIterator(this.tableElement.select("tr"));
+      }
+      this.headings = headings;
+    }
+
+    return iterator;
+  }
+
+  public void close() {
+  }
+
+  /** Iterates over HTML tables, returning an Elements per row. */
+  private class FileReaderIterator implements Iterator<Elements> {
+    Iterator<Element> rowIterator;
+
+    FileReaderIterator(Elements rows) {
+      this.rowIterator = rows.iterator();
+    }
+
+    public boolean hasNext() {
+      return this.rowIterator.hasNext();
+    }
+
+    Elements next(String selector) {
+      Element row = this.rowIterator.next();
+
+      return row.select(selector);
+    }
+
+    // return th and td elements by default
+    public Elements next() {
+      return next("th,td");
+    }
+
+    public void remove() {
+      throw new UnsupportedOperationException("NFW - can't remove!");
+    }
+  }
+}
+
+// End FileReader.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/main/java/org/apache/calcite/adapter/file/FileReaderException.java
----------------------------------------------------------------------
diff --git 
a/file/src/main/java/org/apache/calcite/adapter/file/FileReaderException.java 
b/file/src/main/java/org/apache/calcite/adapter/file/FileReaderException.java
new file mode 100644
index 0000000..faafa69
--- /dev/null
+++ 
b/file/src/main/java/org/apache/calcite/adapter/file/FileReaderException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.apache.calcite.adapter.file;
+
+/**
+ * Indicates that the FileReader failed.
+ */
+class FileReaderException extends Exception {
+  FileReaderException(String message) {
+    super(message);
+  }
+  FileReaderException(String message, Throwable e) {
+    super(message, e);
+  }
+}
+
+// End FileReaderException.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/main/java/org/apache/calcite/adapter/file/FileRowConverter.java
----------------------------------------------------------------------
diff --git 
a/file/src/main/java/org/apache/calcite/adapter/file/FileRowConverter.java 
b/file/src/main/java/org/apache/calcite/adapter/file/FileRowConverter.java
new file mode 100644
index 0000000..4510f3b
--- /dev/null
+++ b/file/src/main/java/org/apache/calcite/adapter/file/FileRowConverter.java
@@ -0,0 +1,393 @@
+/*
+ * 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.apache.calcite.adapter.file;
+
+import org.apache.calcite.adapter.java.JavaTypeFactory;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.util.Pair;
+
+import com.google.common.base.Joiner;
+
+import com.joestelmach.natty.DateGroup;
+import com.joestelmach.natty.Parser;
+
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import java.text.NumberFormat;
+import java.text.ParseException;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+/**
+ * FileRowConverter.
+ */
+class FileRowConverter {
+
+  // cache for lazy initialization
+  private final FileReader fileReader;
+  private final List<Map<String, Object>> fieldConfigs;
+  private boolean initialized = false;
+
+  // row parser configuration
+  private ArrayList<FieldDef> fields;
+
+  /** Creates a FileRowConverter. */
+  FileRowConverter(FileReader fileReader,
+      List<Map<String, Object>> fieldConfigs) {
+    this.fileReader = fileReader;
+    this.fieldConfigs = fieldConfigs;
+  }
+
+  // initialize() - combine HTML table header information with field 
definitions
+  //      to initialize the table reader
+  // NB:  object initialization is deferred to avoid unnecessary URL reads
+  private void initialize() {
+    if (this.initialized) {
+      return;
+    }
+    try {
+      this.fields = new ArrayList<>();
+      final Elements headerElements = this.fileReader.getHeadings();
+
+      // create a name to index map for HTML table elements
+      final Map<String, Integer> headerMap = new HashMap<>();
+      int i = 0;
+      for (Element th : headerElements) {
+        String heading = th.text();
+        if (headerMap.containsKey(heading)) {
+          throw new Exception("duplicate heading: '" + heading + "'");
+        }
+        headerMap.put(heading, i++);
+      }
+
+      // instantiate the field definitions
+      final Set<String> colNames = new HashSet<>();
+      final Set<String> sources = new HashSet<>();
+      if (this.fieldConfigs != null) {
+        try {
+          for (Map<String, Object> fieldConfig : this.fieldConfigs) {
+
+            String thName = (String) fieldConfig.get("th");
+            String name = thName;
+            String newName;
+            FileFieldType type = null;
+            boolean skip = false;
+
+            if (!headerMap.containsKey(thName)) {
+              throw new Exception("bad source column name: '" + thName + "'");
+            }
+            if ((newName = (String) fieldConfig.get("name")) != null) {
+              name = newName;
+            }
+            if (colNames.contains(name)) {
+              throw new Exception("duplicate column name: '" + name + "'");
+            }
+
+            String typeString = (String) fieldConfig.get("type");
+            if (typeString != null) {
+              type = FileFieldType.of(typeString);
+            }
+
+            String sSkip = (String) fieldConfig.get("skip");
+            if (sSkip != null) {
+              skip = Boolean.parseBoolean(sSkip);
+            }
+
+            Integer sourceIx = headerMap.get(thName);
+            colNames.add(name);
+            sources.add(thName);
+            if (!skip) {
+              addFieldDef(name, type, fieldConfig, sourceIx);
+            }
+          }
+        } catch (Exception e) {
+          throw new RuntimeException(e);
+        }
+      }
+
+      // pick up any data elements not explicitly defined
+      for (String name : headerMap.keySet()) {
+        if (!sources.contains(name) && !colNames.contains(name)) {
+          addFieldDef(name, null, null, headerMap.get(name));
+        }
+      }
+
+      // ToDo
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+    this.initialized = true;
+  }
+
+  // add another field definition to the FileRowConverter during initialization
+  private void addFieldDef(String name, FileFieldType type,
+      Map<String, Object> config, int sourceCol) {
+    this.fields.add(new FieldDef(name, type, config, sourceCol));
+  }
+
+  /** Converts a row of JSoup Elements to an array of java objects. */
+  Object toRow(Elements rowElements, int[] projection) {
+    initialize();
+    final Object[] objects = new Object[projection.length];
+
+    for (int i = 0; i < projection.length; i++) {
+      int field = projection[i];
+      objects[i] = this.fields.get(field).convert(rowElements);
+    }
+    return objects;
+  }
+
+  int width() {
+    initialize();
+    return this.fields.size();
+  }
+
+  RelDataType getRowType(JavaTypeFactory typeFactory) {
+    initialize();
+    List<String> names = new ArrayList<>();
+    List<RelDataType> types = new ArrayList<>();
+
+    // iterate through FieldDefs, populating names and types
+    for (FieldDef f : this.fields) {
+      names.add(f.getName());
+
+      FileFieldType fieldType = f.getType();
+      RelDataType type;
+
+      if (fieldType == null) {
+        type = typeFactory.createJavaType(String.class);
+      } else {
+        type = fieldType.toType(typeFactory);
+      }
+
+      types.add(type);
+    }
+
+    if (names.isEmpty()) {
+      names.add("line");
+      types.add(typeFactory.createJavaType(String.class));
+    }
+
+    return typeFactory.createStructType(Pair.zip(names, types));
+  }
+
+  /** Parses an an HTML table cell. */
+  private class CellReader {
+    private String type;
+    private String selector;
+    private Integer selectedElement;
+    private String replaceText;
+    private Pattern replacePattern;
+    private String replaceWith;
+    private String matchText;
+    private Pattern matchPattern;
+    private Integer matchSeq;
+
+    CellReader(Map<String, Object> config) {
+      if (config != null) {
+        this.type = (String) config.get("type");
+        this.selector = (String) config.get("selector");
+        this.selectedElement = (Integer) config.get("selectedElement");
+        this.replaceText = (String) config.get("replace");
+        this.replaceWith = (String) config.get("replaceWith");
+        this.matchText = (String) config.get("match");
+        this.matchSeq = (Integer) config.get("matchSeq");
+      }
+
+      if (this.selector == null) {
+        this.selector = "*";
+      }
+
+      if (this.replaceText != null) {
+        this.replacePattern = Pattern.compile(this.replaceText);
+      }
+
+      if (this.replaceWith == null) {
+        this.replaceWith = "";
+      }
+
+      if (this.matchText != null) {
+        this.matchPattern = Pattern.compile(this.matchText);
+      }
+
+      if (this.matchSeq == null) {
+        this.matchSeq = 0;
+      }
+
+    }
+
+    String read(Element cell) {
+      ArrayList<String> cellText = new ArrayList<>();
+
+      if (this.selectedElement != null) {
+        cellText.add(cell.select(this.selector)
+            .get(this.selectedElement).ownText());
+      } else {
+        for (Element child : cell.select(this.selector)) {
+          //String tagName = child.tag().getName();
+          cellText.add(child.ownText());
+        }
+      }
+
+      String cellString = Joiner.on(" ").join(cellText).trim();
+
+      // replace
+      if (this.replacePattern != null) {
+        Matcher m = this.replacePattern.matcher(cellString);
+        cellString = m.replaceAll(this.replaceWith);
+      }
+
+      // match
+      if (this.matchPattern == null) {
+        return cellString;
+      } else {
+        List<String> allMatches = new ArrayList<>();
+        Matcher m = this.matchPattern.matcher(cellString);
+        while (m.find()) {
+          allMatches.add(m.group());
+        }
+        if (allMatches.size() != 0) {
+          return allMatches.get(this.matchSeq);
+        } else {
+          return null;
+        }
+      }
+    }
+  }
+
+  /** Responsible for managing field (column) definition,
+   * and for converting an Element to a java data type. */
+  private class FieldDef {
+    String name;
+    FileFieldType type;
+    Map<String, Object> config;
+    CellReader cellReader;
+    int cellSeq;
+
+    FieldDef(String name, FileFieldType type, Map<String, Object> config,
+        int cellSeq) {
+      this.name = name;
+      this.type = type;
+      this.config = config;
+      this.cellReader = new CellReader(config);
+      this.cellSeq = cellSeq;
+    }
+
+    Object convert(Elements row) {
+      return toObject(this.type, this.cellReader.read(row.get(this.cellSeq)));
+    }
+
+    public String getName() {
+      return this.name;
+    }
+
+    FileFieldType getType() {
+      return this.type;
+    }
+
+    private java.util.Date parseDate(String string) {
+      Parser parser = new Parser();
+      List groups = parser.parse(string);
+      DateGroup group = (DateGroup) groups.get(0);
+      return group.getDates().get(0);
+    }
+
+    private Object toObject(FileFieldType fieldType, String string) {
+      if ((string == null) || (string.length() == 0)) {
+        return null;
+      }
+
+      if (fieldType == null) {
+        return string;
+      }
+
+      switch (fieldType) {
+      default:
+      case STRING:
+        return string;
+
+      case BOOLEAN:
+        return Boolean.parseBoolean(string);
+
+      case BYTE:
+        return Byte.parseByte(string);
+
+      case SHORT:
+
+        try {
+          return NumberFormat.getIntegerInstance().parse(string)
+              .shortValue();
+        } catch (ParseException e) {
+          return null;
+        }
+
+      case INT:
+
+        try {
+          return NumberFormat.getIntegerInstance().parse(string)
+              .intValue();
+        } catch (ParseException e) {
+          return null;
+        }
+
+      case LONG:
+
+        try {
+          return NumberFormat.getInstance().parse(string).longValue();
+        } catch (ParseException e) {
+          return null;
+        }
+
+      case FLOAT:
+
+        try {
+          return NumberFormat.getInstance().parse(string).floatValue();
+        } catch (ParseException e) {
+          return null;
+        }
+
+      case DOUBLE:
+
+        try {
+          return NumberFormat.getInstance().parse(string).doubleValue();
+        } catch (ParseException e) {
+          return null;
+        }
+
+      case DATE:
+        return new java.sql.Date(parseDate(string).getTime());
+
+      case TIME:
+        return new java.sql.Time(parseDate(string).getTime());
+
+      case TIMESTAMP:
+        return new java.sql.Timestamp(parseDate(string).getTime());
+      }
+    }
+  }
+}
+
+// End FileRowConverter.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/main/java/org/apache/calcite/adapter/file/FileSchema.java
----------------------------------------------------------------------
diff --git a/file/src/main/java/org/apache/calcite/adapter/file/FileSchema.java 
b/file/src/main/java/org/apache/calcite/adapter/file/FileSchema.java
new file mode 100644
index 0000000..6df6171
--- /dev/null
+++ b/file/src/main/java/org/apache/calcite/adapter/file/FileSchema.java
@@ -0,0 +1,67 @@
+/*
+ * 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.apache.calcite.adapter.file;
+
+import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.Table;
+import org.apache.calcite.schema.impl.AbstractSchema;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Schema mapped onto a set of URLs / HTML tables. Each table in the schema
+ * is an HTML table on a URL.
+ */
+class FileSchema extends AbstractSchema {
+  private ImmutableList<Map<String, Object>> tables;
+
+  /**
+   * Creates an HTML tables schema.
+   *
+   * @param parentSchema Parent schema
+   * @param name Schema name
+   * @param tables List containing HTML table identifiers
+   */
+  FileSchema(SchemaPlus parentSchema, String name,
+      List<Map<String, Object>> tables) {
+    this.tables = ImmutableList.copyOf(tables);
+  }
+
+  @Override protected Map<String, Table> getTableMap() {
+    final ImmutableMap.Builder<String, Table> builder = ImmutableMap.builder();
+
+    for (Map<String, Object> tableDef : this.tables) {
+      String tableName = (String) tableDef.get("name");
+
+      try {
+        FileTable table = new FileTable(tableDef, null);
+        builder.put(tableName, table);
+      } catch (Exception e) {
+        e.printStackTrace();
+        System.out.println("Unable to instantiate table for: " + tableName);
+      }
+    }
+
+    return builder.build();
+  }
+}
+
+// End FileSchema.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/main/java/org/apache/calcite/adapter/file/FileSchemaFactory.java
----------------------------------------------------------------------
diff --git 
a/file/src/main/java/org/apache/calcite/adapter/file/FileSchemaFactory.java 
b/file/src/main/java/org/apache/calcite/adapter/file/FileSchemaFactory.java
new file mode 100644
index 0000000..361835f
--- /dev/null
+++ b/file/src/main/java/org/apache/calcite/adapter/file/FileSchemaFactory.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.apache.calcite.adapter.file;
+
+import org.apache.calcite.schema.Schema;
+import org.apache.calcite.schema.SchemaFactory;
+import org.apache.calcite.schema.SchemaPlus;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Factory that creates a {@link FileSchema}.
+ *
+ * <p>Allows a custom schema to be included in a model.json file.
+ * See <a href="http://calcite.apache.org/docs/file.html";>File adapter</a>.
+ */
+@SuppressWarnings("UnusedDeclaration")
+public class FileSchemaFactory implements SchemaFactory {
+  // public constructor, per factory contract
+  public FileSchemaFactory() {
+  }
+
+  public Schema create(SchemaPlus parentSchema, String name,
+      Map<String, Object> operand) {
+    @SuppressWarnings("unchecked") List<Map<String, Object>> tables =
+        (List) operand.get("tables");
+
+    return new FileSchema(parentSchema, name, tables);
+  }
+}
+
+// End FileSchemaFactory.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/main/java/org/apache/calcite/adapter/file/FileTable.java
----------------------------------------------------------------------
diff --git a/file/src/main/java/org/apache/calcite/adapter/file/FileTable.java 
b/file/src/main/java/org/apache/calcite/adapter/file/FileTable.java
new file mode 100644
index 0000000..c8d1a4b
--- /dev/null
+++ b/file/src/main/java/org/apache/calcite/adapter/file/FileTable.java
@@ -0,0 +1,122 @@
+/*
+ * 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.apache.calcite.adapter.file;
+
+import org.apache.calcite.adapter.enumerable.EnumerableConvention;
+import org.apache.calcite.adapter.enumerable.EnumerableTableScan;
+import org.apache.calcite.adapter.java.AbstractQueryableTable;
+import org.apache.calcite.adapter.java.JavaTypeFactory;
+import org.apache.calcite.linq4j.AbstractEnumerable;
+import org.apache.calcite.linq4j.Enumerable;
+import org.apache.calcite.linq4j.Enumerator;
+import org.apache.calcite.linq4j.QueryProvider;
+import org.apache.calcite.linq4j.Queryable;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelProtoDataType;
+import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.Statistic;
+import org.apache.calcite.schema.Statistics;
+import org.apache.calcite.schema.TranslatableTable;
+import org.apache.calcite.schema.impl.AbstractTableQueryable;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * Table implementation wrapping a URL / HTML table.
+ */
+class FileTable extends AbstractQueryableTable
+    implements TranslatableTable {
+
+  private final RelProtoDataType protoRowType;
+  private FileReader reader;
+  private FileRowConverter converter;
+
+  /** Creates a FileTable. */
+  FileTable(Map<String, Object> tableDef, RelProtoDataType protoRowType)
+      throws Exception {
+    super(Object[].class);
+
+    this.protoRowType = protoRowType;
+    @SuppressWarnings("unchecked") ArrayList<Map<String, Object>> fieldConfigs 
=
+        (ArrayList<Map<String, Object>>) tableDef.get("fields");
+    String url = (String) tableDef.get("url");
+    String selector = (String) tableDef.get("selector");
+    Integer index = (Integer) tableDef.get("index");
+    this.reader = new FileReader(url, selector, index);
+    this.converter = new FileRowConverter(this.reader, fieldConfigs);
+    //System.out.println("Created FileTable: " + (String) 
tableDef.get("name"));
+
+  }
+
+  public String toString() {
+    return "FileTable";
+  }
+
+  public Statistic getStatistic() {
+    return Statistics.UNKNOWN;
+  }
+
+  public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+    if (protoRowType != null) {
+      return protoRowType.apply(typeFactory);
+    }
+    return this.converter.getRowType((JavaTypeFactory) typeFactory);
+  }
+
+  public <T> Queryable<T> asQueryable(QueryProvider queryProvider,
+      SchemaPlus schema, String tableName) {
+    return new AbstractTableQueryable<T>(queryProvider, schema, this,
+        tableName) {
+      public Enumerator<T> enumerator() {
+        try {
+          FileEnumerator enumerator =
+              new FileEnumerator(reader.iterator(), converter);
+          //noinspection unchecked
+          return (Enumerator<T>) enumerator;
+        } catch (Exception e) {
+          throw new RuntimeException(e);
+        }
+      }
+    };
+  }
+
+  /** Returns an enumerable over a given projection of the fields. */
+  public Enumerable<Object> project(final int[] fields) {
+    return new AbstractEnumerable<Object>() {
+      public Enumerator<Object> enumerator() {
+        try {
+          return new FileEnumerator(reader.iterator(), converter, fields);
+        } catch (Exception e) {
+          throw new RuntimeException(e);
+        }
+      }
+    };
+  }
+
+  public RelNode toRel(RelOptTable.ToRelContext context,
+      RelOptTable relOptTable) {
+    return new EnumerableTableScan(context.getCluster(),
+        context.getCluster().traitSetOf(EnumerableConvention.INSTANCE),
+        relOptTable, (Class) getElementType());
+  }
+}
+
+// End FileTable.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/main/java/org/apache/calcite/adapter/file/FileTableScan.java
----------------------------------------------------------------------
diff --git 
a/file/src/main/java/org/apache/calcite/adapter/file/FileTableScan.java 
b/file/src/main/java/org/apache/calcite/adapter/file/FileTableScan.java
new file mode 100644
index 0000000..bdd1d50
--- /dev/null
+++ b/file/src/main/java/org/apache/calcite/adapter/file/FileTableScan.java
@@ -0,0 +1,94 @@
+/*
+ * 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.apache.calcite.adapter.file;
+
+import org.apache.calcite.adapter.enumerable.EnumerableConvention;
+import org.apache.calcite.adapter.enumerable.EnumerableRel;
+import org.apache.calcite.adapter.enumerable.EnumerableRelImplementor;
+import org.apache.calcite.adapter.enumerable.PhysType;
+import org.apache.calcite.adapter.enumerable.PhysTypeImpl;
+import org.apache.calcite.linq4j.tree.Blocks;
+import org.apache.calcite.linq4j.tree.Expressions;
+import org.apache.calcite.linq4j.tree.Primitive;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelDataTypeField;
+
+import java.util.List;
+
+/**
+ * Relational expression representing a scan of an HTML table.
+ *
+ * <p>Like any table scan, it serves as a leaf node of a query tree.
+ *
+ * <p>Trivially modified from CsvTableScan.
+ */
+class FileTableScan extends TableScan implements EnumerableRel {
+  private final FileTable webTable;
+  private final int[] fields;
+
+  protected FileTableScan(RelOptCluster cluster, RelOptTable table,
+      FileTable webTable, int[] fields) {
+    super(cluster, cluster.traitSetOf(EnumerableConvention.INSTANCE), table);
+    this.webTable = webTable;
+    this.fields = fields;
+
+    assert webTable != null;
+  }
+
+  @Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+    assert inputs.isEmpty();
+    return new FileTableScan(getCluster(), table, webTable, fields);
+  }
+
+  @Override public RelWriter explainTerms(RelWriter pw) {
+    return super.explainTerms(pw)
+        .item("fields", Primitive.asList(fields));
+  }
+
+  @Override public RelDataType deriveRowType() {
+    final List<RelDataTypeField> fieldList = table.getRowType().getFieldList();
+    final RelDataTypeFactory.FieldInfoBuilder builder =
+        getCluster().getTypeFactory().builder();
+    for (int field : fields) {
+      builder.add(fieldList.get(field));
+    }
+    return builder.build();
+  }
+
+  public Result implement(EnumerableRelImplementor implementor, Prefer pref) {
+    PhysType physType =
+        PhysTypeImpl.of(
+            implementor.getTypeFactory(),
+            getRowType(),
+            pref.preferArray());
+
+    return implementor.result(
+        physType,
+        Blocks.toBlock(
+            Expressions.call(table.getExpression(FileTable.class), "project",
+                Expressions.constant(fields))));
+  }
+}
+
+// End FileTableScan.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/main/java/org/apache/calcite/adapter/file/package-info.java
----------------------------------------------------------------------
diff --git 
a/file/src/main/java/org/apache/calcite/adapter/file/package-info.java 
b/file/src/main/java/org/apache/calcite/adapter/file/package-info.java
new file mode 100644
index 0000000..6a4921e
--- /dev/null
+++ b/file/src/main/java/org/apache/calcite/adapter/file/package-info.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+/**
+ * Calcite query provider that reads from web tables (HTML).
+ *
+ * <p>A Calcite schema that maps onto multiple URLs / HTML Tables.
+ * Each HTML table appears as a table.
+ * Full select SQL operations are available on those tables.
+ */
+package org.apache.calcite.adapter.file;
+
+// End package-info.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/test/java/org/apache/calcite/adapter/file/FileReaderTest.java
----------------------------------------------------------------------
diff --git 
a/file/src/test/java/org/apache/calcite/adapter/file/FileReaderTest.java 
b/file/src/test/java/org/apache/calcite/adapter/file/FileReaderTest.java
new file mode 100644
index 0000000..535011d
--- /dev/null
+++ b/file/src/test/java/org/apache/calcite/adapter/file/FileReaderTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.apache.calcite.adapter.file;
+
+import org.jsoup.select.Elements;
+
+import org.junit.Assume;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+
+/**
+ * Unit tests for FileReader.
+ */
+
+public class FileReaderTest {
+
+  static final String CITIES_URI =
+      
"http://en.wikipedia.org/wiki/List_of_United_States_cities_by_population";;
+
+  static final String STATES_URI =
+      
"http://en.wikipedia.org/wiki/List_of_states_and_territories_of_the_United_States";;
+
+  /**
+   * Test FileReader URL instantiation - no path
+   */
+  @Test
+  public void testFileReaderURLNoPath() throws FileReaderException {
+    Assume.assumeTrue(FileSuite.hazNetwork());
+    FileReader t = new FileReader(STATES_URI);
+    t.refresh();
+  }
+
+  /**
+   * Test FileReader URL instantiation - with path
+   */
+  @Test
+  public void testFileReaderURLWithPath() throws FileReaderException {
+    Assume.assumeTrue(FileSuite.hazNetwork());
+    FileReader t =
+        new FileReader(CITIES_URI,
+            "#mw-content-text > table.wikitable.sortable", 0);
+    t.refresh();
+  }
+
+  /**
+   * Test FileReader URL fetch
+   */
+  @Test
+  public void testFileReaderURLFetch() throws FileReaderException {
+    Assume.assumeTrue(FileSuite.hazNetwork());
+    FileReader t =
+        new FileReader(STATES_URI,
+            "#mw-content-text > table.wikitable.sortable", 0);
+    int i = 0;
+    for (Elements row : t) {
+      i++;
+    }
+    assertTrue(i == 50);
+  }
+
+  /**
+   * Test failed FileReader instantiation - malformed URL
+   */
+  @Test(expected = FileReaderException.class)
+  public void testFileReaderMalURL() throws FileReaderException {
+    FileReader t = new FileReader("bad" + CITIES_URI, "table:eq(4)");
+    t.refresh();
+  }
+
+  /**
+   * Test failed FileReader instantiation - bad URL
+   */
+  @Test(expected = FileReaderException.class)
+  public void testFileReaderBadURL() throws FileReaderException {
+    final String uri =
+        
"http://ex.wikipedia.org/wiki/List_of_United_States_cities_by_population";;
+    FileReader t = new FileReader(uri, "table:eq(4)");
+    t.refresh();
+  }
+
+  /**
+   * Test failed FileReader instantiation - bad selector
+   */
+  @Test(expected = FileReaderException.class)
+  public void testFileReaderBadSelector() throws FileReaderException {
+    FileReader t =
+        new FileReader("file:target/test-classes/tableOK.html", "table:eq(1)");
+    t.refresh();
+  }
+
+  /**
+   * Test FileReader with static file - headings
+   */
+  @Test
+  public void testFileReaderHeadings() throws FileReaderException {
+    FileReader t = new FileReader("file:target/test-classes/tableOK.html");
+    Elements headings = t.getHeadings();
+    assertTrue(headings.get(1).text().equals("H1"));
+  }
+
+  /**
+   * Test FileReader with static file - data
+   */
+  @Test
+  public void testFileReaderData() throws FileReaderException {
+    FileReader t = new FileReader("file:target/test-classes/tableOK.html");
+    Iterator<Elements> i = t.iterator();
+    Elements row = i.next();
+    assertTrue(row.get(2).text().equals("R0C2"));
+    row = i.next();
+    assertTrue(row.get(0).text().equals("R1C0"));
+  }
+
+  /**
+   * Test FileReader with bad static file - headings
+   */
+  @Test
+  public void testFileReaderHeadingsBadFile() throws FileReaderException {
+    FileReader t =
+        new FileReader("file:target/test-classes/tableNoTheadTbody.html");
+    Elements headings = t.getHeadings();
+    assertTrue(headings.get(1).text().equals("H1"));
+  }
+
+  /**
+   * Test FileReader with bad static file - data
+   */
+  @Test
+  public void testFileReaderDataBadFile() throws FileReaderException {
+    final FileReader t =
+        new FileReader("file:target/test-classes/tableNoTheadTbody.html");
+    Iterator<Elements> i = t.iterator();
+    Elements row = i.next();
+    assertTrue(row.get(2).text().equals("R0C2"));
+    row = i.next();
+    assertTrue(row.get(0).text().equals("R1C0"));
+  }
+
+  /**
+   * Test FileReader with no headings static file - data
+   */
+  @Test
+  public void testFileReaderDataNoTH() throws FileReaderException {
+    FileReader t = new FileReader("file:target/test-classes/tableNoTH.html");
+    Iterator<Elements> i =
+        new FileReader("file:target/test-classes/tableNoTH.html").iterator();
+    Elements row = i.next();
+    assertTrue(row.get(2).text().equals("R0C2"));
+  }
+
+  /**
+   * Test FileReader iterator with static file
+   */
+  @Test
+  public void testFileReaderIterator() throws FileReaderException {
+    FileReader t = new FileReader("file:target/test-classes/tableOK.html");
+    Elements row = null;
+    for (Elements aT : t) {
+      row = aT;
+    }
+    assertFalse(row == null);
+    assertTrue(row.get(1).text().equals("R2C1"));
+  }
+
+}
+
+// End FileReaderTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/test/java/org/apache/calcite/adapter/file/FileSuite.java
----------------------------------------------------------------------
diff --git a/file/src/test/java/org/apache/calcite/adapter/file/FileSuite.java 
b/file/src/test/java/org/apache/calcite/adapter/file/FileSuite.java
new file mode 100644
index 0000000..ffaa791
--- /dev/null
+++ b/file/src/test/java/org/apache/calcite/adapter/file/FileSuite.java
@@ -0,0 +1,58 @@
+/*
+ * 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.apache.calcite.adapter.file;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+import java.io.IOException;
+
+import java.net.Socket;
+
+/**
+ * Unit test suite for Calcite File adapter.
+ */
+@RunWith(Suite.class)
[email protected]({ FileReaderTest.class, SqlTest.class })
+public class FileSuite {
+  private FileSuite() {}
+
+  private static final String TEST_HOST = "en.wikipedia.org";
+
+  static boolean hazNetwork() {
+    Socket socket = null;
+    boolean reachable = false;
+    try {
+      socket = new Socket(FileSuite.TEST_HOST, 80);
+      reachable = true;
+    } catch (Exception e) {
+      // do nothing
+    } finally {
+      if (socket != null) {
+        try {
+          socket.close();
+        } catch (IOException e) {
+          // do nothing
+        }
+      }
+    }
+    return reachable;
+  }
+
+}
+
+// End FileSuite.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/test/java/org/apache/calcite/adapter/file/SqlTest.java
----------------------------------------------------------------------
diff --git a/file/src/test/java/org/apache/calcite/adapter/file/SqlTest.java 
b/file/src/test/java/org/apache/calcite/adapter/file/SqlTest.java
new file mode 100644
index 0000000..60549ed
--- /dev/null
+++ b/file/src/test/java/org/apache/calcite/adapter/file/SqlTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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.apache.calcite.adapter.file;
+
+import com.google.common.base.Function;
+
+import org.junit.Assume;
+import org.junit.Test;
+
+import java.io.PrintStream;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * System test of the Calcite file adapter, which can also read and parse
+ * HTML tables over HTTP.
+ */
+public class SqlTest {
+  // helper functions
+
+  private void checkSql(String model, String sql) throws SQLException {
+    checkSql(sql, model, new Function<ResultSet, Void>() {
+      public Void apply(ResultSet resultSet) {
+        try {
+          output(resultSet, System.out);
+        } catch (SQLException e) {
+          throw new RuntimeException(e);
+        }
+        return null;
+      }
+    });
+  }
+
+  private void checkSql(String model, String sql, final String expected)
+      throws SQLException {
+    checkSql(sql, model, new Function<ResultSet, Void>() {
+      public Void apply(ResultSet resultSet) {
+        try {
+          String actual = SqlTest.toString(resultSet);
+          if (!expected.equals(actual)) {
+            System.out.println("Assertion failure:");
+            System.out.println("\tExpected: '" + expected + "'");
+            System.out.println("\tActual: '" + actual + "'");
+          }
+          assertEquals(expected, actual);
+        } catch (SQLException e) {
+          throw new RuntimeException(e);
+        }
+        return null;
+      }
+    });
+  }
+
+  private void checkSql(String sql, String model, Function<ResultSet, Void> fn)
+      throws SQLException {
+    Connection connection = null;
+    Statement statement = null;
+    try {
+      Properties info = new Properties();
+      info.put("model", "target/test-classes/" + model + ".json");
+      connection = DriverManager.getConnection("jdbc:calcite:", info);
+      statement = connection.createStatement();
+      final ResultSet resultSet =
+          statement.executeQuery(
+              sql);
+      fn.apply(resultSet);
+    } finally {
+      close(connection, statement);
+    }
+  }
+
+  private static String toString(ResultSet resultSet) throws SQLException {
+    StringBuilder buf = new StringBuilder();
+    while (resultSet.next()) {
+      int n = resultSet.getMetaData().getColumnCount();
+      String sep = "";
+      for (int i = 1; i <= n; i++) {
+        buf.append(sep)
+            .append(resultSet.getMetaData().getColumnLabel(i))
+            .append("=")
+            .append(resultSet.getObject(i));
+        sep = "; ";
+      }
+      buf.append("\n");
+    }
+    return buf.toString();
+  }
+
+  private void output(ResultSet resultSet, PrintStream out)
+      throws SQLException {
+    final ResultSetMetaData metaData = resultSet.getMetaData();
+    final int columnCount = metaData.getColumnCount();
+    while (resultSet.next()) {
+      for (int i = 1;; i++) {
+        out.print(resultSet.getString(i));
+        if (i < columnCount) {
+          out.print(", ");
+        } else {
+          out.println();
+          break;
+        }
+      }
+    }
+  }
+
+  private void close(Connection connection, Statement statement) {
+    if (statement != null) {
+      try {
+        statement.close();
+      } catch (SQLException e) {
+        // ignore
+      }
+    }
+    if (connection != null) {
+      try {
+        connection.close();
+      } catch (SQLException e) {
+        // ignore
+      }
+    }
+  }
+
+  // tests
+
+  /**
+   * Reads from a local file and checks the result
+   */
+  @Test
+  public void testFileSelect() throws SQLException {
+    checkSql("testModel", "select H1 from T1 where H0 = 'R1C0'", "H1=R1C1\n");
+  }
+
+  /**
+   * Reads from a local file without table headers <TH> and checks the result
+   */
+  @Test
+  public void testNoTHSelect() throws SQLException {
+    Assume.assumeTrue(FileSuite.hazNetwork());
+    checkSql("testModel",
+        "select \"col1\" from T1_NO_TH where \"col0\" like 'R0%'",
+        "col1=R0C1\n");
+  }
+
+  /**
+   * Reads from a local file - finds larger table even without <TH> elements
+   */
+  @Test
+  public void testFindBiggerNoTH() throws SQLException {
+    checkSql("testModel",
+        "select \"col4\" from TABLEX2 where \"col0\" like 'R1%'",
+        "col4=R1C4\n");
+  }
+
+  /**
+   * Reads from a URL and checks the result
+   */
+  @Test
+  public void testURLSelect() throws SQLException {
+    Assume.assumeTrue(FileSuite.hazNetwork());
+    final String sql = "select \"State\", \"Statehood\" from \"States\"\n"
+        + "where \"State\" = 'California'";
+    checkSql("wiki", sql,
+        "State=California; Statehood=1850-09-09\n");
+  }
+
+}
+
+// End SqlTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/test/java/org/apache/calcite/adapter/file/package-info.java
----------------------------------------------------------------------
diff --git 
a/file/src/test/java/org/apache/calcite/adapter/file/package-info.java 
b/file/src/test/java/org/apache/calcite/adapter/file/package-info.java
new file mode 100644
index 0000000..edb702f
--- /dev/null
+++ b/file/src/test/java/org/apache/calcite/adapter/file/package-info.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+/**
+ * Query provider that reads from files and web pages in various formats.
+ *
+ * <p>A Calcite schema that maps onto multiple URLs / HTML Tables.  Each HTML
+ * table appears as a table.  Full select SQL operations are available on those
+ * tables.
+ */
+package org.apache.calcite.adapter.file;
+
+// End package-info.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/test/resources/sales.json
----------------------------------------------------------------------
diff --git a/file/src/test/resources/sales.json 
b/file/src/test/resources/sales.json
new file mode 100644
index 0000000..9f8f2ec
--- /dev/null
+++ b/file/src/test/resources/sales.json
@@ -0,0 +1,18 @@
+{
+  "version": "1.0",
+  "defaultSchema": "SALES",
+  "schemas": [ {
+    "name": "SALES",
+    "type": "custom",
+    "factory": "org.apache.calcite.adapter.file.FileSchemaFactory",
+    "operand": {
+      "tables": [ {
+        "name": "EMPS",
+        "url": "file:file/src/test/resources/sales/EMPS.html"
+      }, {
+        "name": "DEPTS",
+        "url": "file:file/src/test/resources/sales/DEPTS.html"
+      } ]
+    }
+  } ]
+}

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/test/resources/sales/DEPTS.html
----------------------------------------------------------------------
diff --git a/file/src/test/resources/sales/DEPTS.html 
b/file/src/test/resources/sales/DEPTS.html
new file mode 100644
index 0000000..302d443
--- /dev/null
+++ b/file/src/test/resources/sales/DEPTS.html
@@ -0,0 +1,42 @@
+<!--
+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.
+-->
+<html>
+  <body>
+    <table>
+      <thead>
+        <tr>
+          <th>DEPTNO</th>
+          <th>NAME</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td>10</td>
+          <td>Sales</td>
+        </tr>
+        <tr>
+          <td>20</td>
+          <td>Marketing</td>
+        </tr>
+        <tr>
+          <td>30</td>
+          <td>Accounts</td>
+        </tr>
+      </tbody>
+    </table>
+  </body>
+</html>

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/test/resources/sales/EMPS.html
----------------------------------------------------------------------
diff --git a/file/src/test/resources/sales/EMPS.html 
b/file/src/test/resources/sales/EMPS.html
new file mode 100644
index 0000000..05b166d
--- /dev/null
+++ b/file/src/test/resources/sales/EMPS.html
@@ -0,0 +1,56 @@
+<!--
+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.
+-->
+<html>
+  <body>
+    <table>
+      <thead>
+        <tr>
+          <th>EMPNO</th>
+          <th>NAME</th>
+          <th>DEPTNO</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td>100</td>
+          <td>Fred</td>
+          <td>30</td>
+        </tr>
+        <tr>
+          <td>110</td>
+          <td>Eric</td>
+          <td>20</td>
+        </tr>
+        <tr>
+          <td>110</td>
+          <td>John</td>
+          <td>40</td>
+        </tr>
+        <tr>
+          <td>120</td>
+          <td>Wilma</td>
+          <td>20</td>
+        </tr>
+        <tr>
+          <td>130</td>
+          <td>Alice</td>
+          <td>40</td>
+        </tr>
+      </tbody>
+    </table>
+  </body>
+</html>

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/test/resources/tableNoTH.html
----------------------------------------------------------------------
diff --git a/file/src/test/resources/tableNoTH.html 
b/file/src/test/resources/tableNoTH.html
new file mode 100644
index 0000000..073ab8e
--- /dev/null
+++ b/file/src/test/resources/tableNoTH.html
@@ -0,0 +1,39 @@
+<html>
+<!--
+{% comment %}
+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.
+{% endcomment %}
+-->
+<body>
+<table>
+<tr>
+<td>R0C0</td>
+<td>R0C1</td>
+<td>R0C2</td>
+</tr>
+<tr>
+<td>R1C0</td>
+<td>R1C1</td>
+<td>R1C2</td>
+</tr>
+<tr>
+<td>R2C0</td>
+<td>R2C1</td>
+<td>R2C2</td>
+</tr>
+</table>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/test/resources/tableNoTheadTbody.html
----------------------------------------------------------------------
diff --git a/file/src/test/resources/tableNoTheadTbody.html 
b/file/src/test/resources/tableNoTheadTbody.html
new file mode 100644
index 0000000..9e77d88
--- /dev/null
+++ b/file/src/test/resources/tableNoTheadTbody.html
@@ -0,0 +1,44 @@
+<html>
+<!--
+{% comment %}
+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.
+{% endcomment %}
+-->
+<body>
+<table>
+<tr>
+<th>H0</th>
+<th>H1</th>
+<th>H2</th>
+</tr>
+<tr>
+<td>R0C0</td>
+<td>R0C1</td>
+<td>R0C2</td>
+</tr>
+<tr>
+<td>R1C0</td>
+<td>R1C1</td>
+<td>R1C2</td>
+</tr>
+<tr>
+<td>R2C0</td>
+<td>R2C1</td>
+<td>R2C2</td>
+</tr>
+</table>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/test/resources/tableOK.html
----------------------------------------------------------------------
diff --git a/file/src/test/resources/tableOK.html 
b/file/src/test/resources/tableOK.html
new file mode 100644
index 0000000..8b46aed
--- /dev/null
+++ b/file/src/test/resources/tableOK.html
@@ -0,0 +1,30 @@
+<html>
+<body>
+<table>
+<thead>
+<tr>
+<th>H0</th>
+<th>H1</th>
+<th>H2</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>R0C0</td>
+<td>R0C1</td>
+<td>R0C2</td>
+</tr>
+<tr>
+<td>R1C0</td>
+<td>R1C1</td>
+<td>R1C2</td>
+</tr>
+<tr>
+<td>R2C0</td>
+<td>R2C1</td>
+<td>R2C2</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/test/resources/tableX2.html
----------------------------------------------------------------------
diff --git a/file/src/test/resources/tableX2.html 
b/file/src/test/resources/tableX2.html
new file mode 100644
index 0000000..d3bf8d6
--- /dev/null
+++ b/file/src/test/resources/tableX2.html
@@ -0,0 +1,69 @@
+<html>
+<!--
+{% comment %}
+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.
+{% endcomment %}
+-->
+<body>
+<table id="smaller">
+<thead>
+<tr>
+<th>H0</th>
+<th>H1</th>
+<th>H2</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>R0C0</td>
+<td>R0C1</td>
+<td>R0C2</td>
+</tr>
+<tr>
+<td>R1C0</td>
+<td>R1C1</td>
+<td>R1C2</td>
+</tr>
+</tbody>
+</table>
+<table id="bigger">
+<tr>
+<td>R0C0</td>
+<td>R0C1</td>
+<td>R0C2</td>
+<td>R0C3</td>
+<td>R0C4</td>
+<td>R0C5</td>
+</tr>
+<tr>
+<td>R1C0</td>
+<td>R1C1</td>
+<td>R1C2</td>
+<td>R1C3</td>
+<td>R1C4</td>
+<td>R1C5</td>
+</tr>
+<tr>
+<td>R2C0</td>
+<td>R2C1</td>
+<td>R2C2</td>
+<td>R2C3</td>
+<td>R2C4</td>
+<td>R2C5</td>
+</tr>
+</table>
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/test/resources/testModel.json
----------------------------------------------------------------------
diff --git a/file/src/test/resources/testModel.json 
b/file/src/test/resources/testModel.json
new file mode 100644
index 0000000..f88b661
--- /dev/null
+++ b/file/src/test/resources/testModel.json
@@ -0,0 +1,21 @@
+{
+  "version": "1.0",
+  "defaultSchema": "TEST",
+  "schemas": [ {
+    "name": "TEST",
+    "type": "custom",
+    "factory": "org.apache.calcite.adapter.file.FileSchemaFactory",
+    "operand": {
+      "tables": [ {
+        "name": "T1",
+        "url": "file:target/test-classes/tableOK.html"
+      }, {
+        "name": "T1_NO_TH",
+        "url": "file:target/test-classes/tableNoTH.html"
+      }, {
+        "name": "TABLEX2",
+        "url": "file:target/test-classes/tableX2.html"
+      } ]
+    }
+  } ]
+}

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/test/resources/webjoin.sql
----------------------------------------------------------------------
diff --git a/file/src/test/resources/webjoin.sql 
b/file/src/test/resources/webjoin.sql
new file mode 100644
index 0000000..11a54de
--- /dev/null
+++ b/file/src/test/resources/webjoin.sql
@@ -0,0 +1,25 @@
+# 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.
+#
+!connect jdbc:calcite:model=target/test-classes/wiki.json admin admin
+
+values 'What are the largest cities in California?';
+select c."Rank", c."City", c."State", c."Population" "City Population", 
s."Population" "State Population", (100 * c."Population" / s."Population") "Pct 
State Population" from "Cities" c, "States" s where c."State" = s."State" and 
s."State" = 'California';
+
+values 'What percentage of California residents live in big cities?';
+select count(*) "City Count", sum(100 * c."Population" / s."Population") "Pct 
State Population" from "Cities" c, "States" s where c."State" = s."State" and 
s."State" = 'California';
+
+values 'What cities comprise the largest percentage of state population?';
+select c."Rank", c."City", c."State", c."Population" "City Population", 
s."Population" "State Population", (100 * c."Population" / s."Population") "Pct 
State Population" from "Cities" c, "States" s where c."State" = s."State" order 
by "Pct State Population" desc limit 10;

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/file/src/test/resources/wiki.json
----------------------------------------------------------------------
diff --git a/file/src/test/resources/wiki.json 
b/file/src/test/resources/wiki.json
new file mode 100644
index 0000000..36654e9
--- /dev/null
+++ b/file/src/test/resources/wiki.json
@@ -0,0 +1,93 @@
+{
+/*
+ * Example calcite-file schema using wikipedia pages for us states and
+ * cities.
+ */
+  version: "1.0",
+  defaultSchema: "WIKI",
+  /*
+   * Schema Definition
+   *
+   * Schemata are defined as a list of tables, each containing,
+   * minimally a table name and a url.  Optionally, table definitions
+   * may also include a selector and index to specify the desired
+   * table for those pages with many.  Absent a table specification,
+   * calcite-file will choose the largest table on the page.
+   * calcite-file uses Jsoup (http://jsoup.org/) for HTML DOM
+   * navigation and selectors for both tables and fields follow the
+   * Jsoup selector specification
+   * (http://jsoup.org/cookbook/extracting-data/selector-syntax).
+   *
+   * Also optional is the definition of a list of fields for the table
+   * which can increase the usability of the table (note the
+   * difference between RawCities and Cities below).  Field
+   * definitions may be used to rename or skip source fields, to
+   * select and condition the cell contents and to set a data type.
+   *
+   * Parsing Cell Contents
+   *
+   * calcite-file includes the capability to select DOM nodes within a
+   * cell, to replace text within the selected element, to match
+   * within the selected text, and choose a data type for the
+   * resulting database column.  Processing steps are applied in the
+   * order described and replace and match patterns are based on Java
+   * Pattern syntax see:
+   * (http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html).
+   *
+   */
+  "schemas": [ {
+    "name": "WIKI",
+    "type": "custom",
+    "factory": "org.apache.calcite.adapter.file.FileSchemaFactory",
+    "operand": {
+      "tables": [ {
+        "name": "RawCities",
+        "url": 
"http://en.wikipedia.org/wiki/List_of_United_States_cities_by_population";,
+        "selector": "#mw-content-text > table.wikitable.sortable",
+        "index": 0
+      }, {
+        "name": "Cities",
+        "url": 
"http://en.wikipedia.org/wiki/List_of_United_States_cities_by_population";,
+        "selector": "#mw-content-text > table.wikitable.sortable",
+        "index": 0,
+        "fields": [
+          // note the use of a pattern match in the following
+          // matchSeq is optional and defaults to 0
+          {"th": "2012 rank", "name": "Rank", "type": "int", "match": "\\d+", 
"matchSeq": 0},
+          {"th": "City", "selector": "a", "selectedElement": 0},
+          {"th": "State[5]", "name": "State", "selector": "a:eq(0)"},
+          {"th": "2012 estimate", "name": "Population", "type": "double"},
+          // NB: we're skipping the following two fields
+          {"th": "2010 Census", "skip": "true"},
+          {"th": "Change", "skip": "true"},
+          // here, we're using a selector to navigate the dom inside the table 
cell
+          {"th": "2012 land area", "name": "Land Area", "type": "double", 
"selector": ":not(span)"},
+          {"th": "2012 population density", "skip": "true"},
+          // eliminate the degree symbol from the location field
+          // note - it's now possible to create multiple columns from one HTML 
column
+          {"th": "Location", "name": "Latitude", "replace": "\u00B0", 
"replaceWith": " ",
+              "match": "\\d+\\.\\d+", "type": "double"},
+          {"th": "Location", "name": "Longitude", "replace": "\u00B0", 
"replaceWith": " ",
+              "match": "\\d+\\.\\d+", "matchSeq": 1, "type": "double"},
+          // note that field definitions do not need to be in HTML table order 
- just makes it easier
+          {"th": "ANSI", "skip": "true"}
+        ]
+      }, {
+        "name": "States",
+        "url": 
"http://en.wikipedia.org/wiki/List_of_states_and_territories_of_the_United_States";,
+        "selector": "#mw-content-text > table.wikitable.sortable",
+        "index": 0,
+        "fields": [
+          {"th": "State", "selector": "a", "selectedElement": 0},
+          {"th": "Largest city[A][10]", "name": "Largest City"},
+          {"th": "Statehood[B]", "name": "Statehood", "type": "Date"},
+          {"th": "Population (2013 est)[11]", "name": "Population", "type": 
"double"},
+          {"th": "Total area in mi2 (km2)[C][12]", "name": "Total Area", 
"type": "double"},
+          {"th": "Land area in mi2 (km2)[C][12]", "name": "Land Area", "type": 
"double"},
+          {"th": "Water area in mi2 (km2)[C][12]", "name": "Water Area",
+              "type": "double"}
+        ]
+      } ]
+    }
+  } ]
+}

http://git-wip-us.apache.org/repos/asf/calcite/blob/cf46d3be/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 6f683ac..977b8ea 100644
--- a/pom.xml
+++ b/pom.xml
@@ -90,6 +90,7 @@ limitations under the License.
     <jcip-annotations.version>1.0-1</jcip-annotations.version>
     <jetty.version>9.2.15.v20160210</jetty.version>
     <jmh.version>1.12</jmh.version>
+    <jsoup.version>1.10.2</jsoup.version>
     <junit.version>4.12</junit.version>
     <maven-checkstyle-plugin.version>2.12.1</maven-checkstyle-plugin.version>
     <maven-dependency-plugin.version>2.10</maven-dependency-plugin.version>
@@ -100,6 +101,7 @@ limitations under the License.
     <mockito-all.version>1.10.19</mockito-all.version>
     <mongo-java-driver.version>2.12.3</mongo-java-driver.version>
     <mysql-driver.version>5.1.20</mysql-driver.version>
+    <natty.version>0.13</natty.version>
     <opencsv.version>2.3</opencsv.version>
     <oracle-jdbc6-driver.version>11.2.0.2.0</oracle-jdbc6-driver.version>
     <aggdesigner.version>6.0</aggdesigner.version>
@@ -133,6 +135,7 @@ limitations under the License.
     <module>druid</module>
     <module>elasticsearch</module>
     <module>example</module>
+    <module>file</module>
     <module>linq4j</module>
     <module>mongodb</module>
     <module>piglet</module>
@@ -173,6 +176,11 @@ limitations under the License.
       </dependency>
       <dependency>
         <groupId>org.apache.calcite</groupId>
+        <artifactId>calcite-example-csv</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.calcite</groupId>
         <artifactId>calcite-linq4j</artifactId>
         <version>${project.version}</version>
       </dependency>
@@ -224,6 +232,11 @@ limitations under the License.
         <version>${guava.version}</version>
       </dependency>
       <dependency>
+        <groupId>com.joestelmach</groupId>
+        <artifactId>natty</artifactId>
+        <version>${natty.version}</version>
+      </dependency>
+      <dependency>
         <groupId>com.oracle</groupId>
         <artifactId>ojdbc6</artifactId>
         <version>${oracle-jdbc6-driver.version}</version>
@@ -309,6 +322,11 @@ limitations under the License.
         <version>${httpcore.version}</version>
       </dependency>
       <dependency>
+        <groupId>org.jsoup</groupId>
+        <artifactId>jsoup</artifactId>
+        <version>${jsoup.version}</version>
+      </dependency>
+      <dependency>
         <groupId>org.mockito</groupId>
         <artifactId>mockito-all</artifactId>
         <version>${mockito-all.version}</version>

Reply via email to