You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/11/07 09:36:46 UTC

[sling-org-apache-sling-hapi] 01/24: SLING-5136 - HApi microdata client, contributed by Andrei Dulvac, thanks!

This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.hapi.client-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-hapi.git

commit 01c66077c21f293a2fbb37ec19c194c174e18e30
Author: Bertrand Delacretaz <bd...@apache.org>
AuthorDate: Tue Oct 13 08:56:08 2015 +0000

    SLING-5136 - HApi microdata client, contributed by Andrei Dulvac, thanks!
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/contrib/extensions/hapi/client@1708306 13f79535-47bb-0310-9956-ffa450edef68
---
 .gitignore                                         |  15 +
 pom.xml                                            |  97 +++++
 .../apache/sling/hapi/client/ClientException.java  |  32 ++
 .../org/apache/sling/hapi/client/Document.java     |  29 ++
 .../org/apache/sling/hapi/client/HtmlClient.java   |  72 ++++
 .../java/org/apache/sling/hapi/client/Item.java    |  82 +++++
 .../java/org/apache/sling/hapi/client/Items.java   |  35 ++
 .../apache/sling/hapi/client/forms/FormValues.java | 116 ++++++
 .../org/apache/sling/hapi/client/forms/Vals.java   |  60 ++++
 .../hapi/client/impl/AbstractHtmlClientImpl.java   | 192 ++++++++++
 .../hapi/client/microdata/MicrodataDocument.java   | 399 +++++++++++++++++++++
 .../hapi/client/microdata/MicrodataHtmlClient.java |  44 +++
 src/test/java/FormTest.java                        | 114 ++++++
 src/test/java/GetPostTest.java                     | 111 ++++++
 src/test/java/ItemsTest.java                       | 121 +++++++
 src/test/resources/items.html                      |  24 ++
 src/test/resources/items_forms.html                |  28 ++
 src/test/resources/items_links.html                |  20 ++
 18 files changed, 1591 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..518c151
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+target/
+.project
+.classpath
+.settings/
+/sling/
+derby.log
+*.iml
+*.ipr
+*.iws
+coverage.em
+coverage.ec
+coverage/
+.DS_Store
+.idea/
+bundles/extensions/models/integration-tests/sling
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..9d95953
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+~ 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.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>20</version>
+        <relativePath/>
+    </parent>
+
+    <artifactId>org.apache.sling.hapi.client</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+
+    <name>HApi - Sling Hypermedia API client-side tools</name>
+    <description>Sling client-side tools for HApi</description>
+
+    <properties>
+        <sling.java.version>6</sling.java.version>
+    </properties>
+
+    <build>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.5.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpmime</artifactId>
+            <version>4.5</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+            <version>2.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>1.3.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jsoup</groupId>
+            <artifactId>jsoup</artifactId>
+            <version>1.8.3</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.7.12</version>
+        </dependency>
+        <!-- test scope -->
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.5.1</version>
+            <classifier>tests</classifier>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>1.7.12</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.12</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/src/main/java/org/apache/sling/hapi/client/ClientException.java b/src/main/java/org/apache/sling/hapi/client/ClientException.java
new file mode 100644
index 0000000..9bda81e
--- /dev/null
+++ b/src/main/java/org/apache/sling/hapi/client/ClientException.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.sling.hapi.client;
+
+/**
+ * An HTTP client exception
+ */
+public class ClientException extends Exception {
+    public ClientException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ClientException(String message) {
+        super(message);
+    }
+}
diff --git a/src/main/java/org/apache/sling/hapi/client/Document.java b/src/main/java/org/apache/sling/hapi/client/Document.java
new file mode 100644
index 0000000..9344e5d
--- /dev/null
+++ b/src/main/java/org/apache/sling/hapi/client/Document.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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.sling.hapi.client;
+
+/**
+ * An HTML document representation
+ */
+public interface Document {
+    Items link(String rel) throws ClientException;
+    Items form(String rel) throws ClientException;
+    Items item(String rel) throws ClientException;
+    Items items() throws ClientException;
+}
diff --git a/src/main/java/org/apache/sling/hapi/client/HtmlClient.java b/src/main/java/org/apache/sling/hapi/client/HtmlClient.java
new file mode 100644
index 0000000..078f46b
--- /dev/null
+++ b/src/main/java/org/apache/sling/hapi/client/HtmlClient.java
@@ -0,0 +1,72 @@
+/*******************************************************************************
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.sling.hapi.client;
+
+import org.apache.http.HttpEntity;
+
+public interface HtmlClient {
+
+    /**
+     * Enters a url and return a Document
+     *
+     * @param url
+     * @return
+     * @throws ClientException
+     */
+    <T extends Document> T enter(String url) throws ClientException;
+
+    /**
+     * Performs a GET request and returns a Document
+     *
+     * @param url the URL String to perform an HTTP GET on
+     * @return
+     * @throws ClientException
+     */
+    <T extends Document> T get(String url) throws ClientException;
+
+
+    /**
+     * Performs a POST request.
+     *
+     * @param url    the URL String to perform an HTTP post on
+     * @param entity data to post
+     * @return
+     * @throws ClientException
+     */
+    <T extends Document> T post(String url, HttpEntity entity) throws ClientException;
+
+    /**
+     * Performs a DELETE request and returns a Document
+     *
+     * @param url the URL String to perform an HTTP DELETE on
+     * @return
+     * @throws ClientException
+     */
+    <T extends Document> T delete(String url) throws ClientException;
+
+    /**
+     * Method to create a new Document representation from an HTML content String
+     * @param html
+     * @param <T>
+     * @return
+     */
+    <T extends Document> T newDocument(String html);
+
+}
diff --git a/src/main/java/org/apache/sling/hapi/client/Item.java b/src/main/java/org/apache/sling/hapi/client/Item.java
new file mode 100644
index 0000000..f1d8865
--- /dev/null
+++ b/src/main/java/org/apache/sling/hapi/client/Item.java
@@ -0,0 +1,82 @@
+package org.apache.sling.hapi.client;
+
+/*******************************************************************************
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ ******************************************************************************/
+
+import org.apache.http.NameValuePair;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ *
+ */
+public interface Item {
+    /**
+     * Returns the property of the item having the given name.
+     */
+    Items prop(String name) throws ClientException;
+
+    /**
+     * Return a List of all the properties of this item
+     */
+    Set<String> props() throws ClientException;
+
+    /**
+     * Returns the child links that have the given relation
+     */
+    Items link(String rel) throws ClientException;
+
+    /**
+     * Returns the child forms that have the given relation
+     */
+    Items form(String rel) throws ClientException;
+
+    /**
+     * Returns the text value of the property.
+     */
+    String text() throws ClientException;
+
+    /**
+     * Returns he boolean value of the property
+     */
+    boolean bool() throws ClientException;
+
+    int number() throws ClientException;
+
+    /**
+     * Returns the href value of the property. This only makes sense for hyperlinks (&lt;a&gt; or &lt;link&gt;).
+     */
+    String href();
+
+    /**
+     * Returns the <i>src</i> value of the property.
+     */
+    String src();
+
+    /**
+     * Follow a hyperlink and get a new {@see Document} representation
+     */
+    Document follow() throws ClientException;
+
+    /**
+     * Submits this form item and returns a new {@see Document} representation
+     */
+    Document submit(Iterable<NameValuePair> data) throws ClientException;
+}
diff --git a/src/main/java/org/apache/sling/hapi/client/Items.java b/src/main/java/org/apache/sling/hapi/client/Items.java
new file mode 100644
index 0000000..4ccbc4d
--- /dev/null
+++ b/src/main/java/org/apache/sling/hapi/client/Items.java
@@ -0,0 +1,35 @@
+package org.apache.sling.hapi.client;
+
+/*******************************************************************************
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ ******************************************************************************/
+
+/**
+ * A composite object representing items in a {@see Document}
+ */
+public interface Items extends Iterable<Item>, Item {
+    /**
+     * Returns the item at one index
+     */
+    Item at(int index) throws IndexOutOfBoundsException;
+
+    /**
+     * Returns the amount of items contained in this collection.
+     */
+    int length();
+}
diff --git a/src/main/java/org/apache/sling/hapi/client/forms/FormValues.java b/src/main/java/org/apache/sling/hapi/client/forms/FormValues.java
new file mode 100644
index 0000000..9a12cb1
--- /dev/null
+++ b/src/main/java/org/apache/sling/hapi/client/forms/FormValues.java
@@ -0,0 +1,116 @@
+/*******************************************************************************
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.sling.hapi.client.forms;
+
+import java.io.UnsupportedEncodingException;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.message.BasicNameValuePair;
+import org.jsoup.nodes.Element;
+
+/**
+ *
+ */
+public class FormValues {
+    private Element form;
+
+    private Iterable<NameValuePair> values;
+
+    private Vals list = new Vals();
+
+    public FormValues(Element form, Iterable<NameValuePair> values) {
+        this.form = form;
+        this.values = values;
+
+        build();
+        resolve();
+    }
+
+    /**
+     * @return
+     * {@see http://www.w3.org/TR/html5/forms.html#constructing-the-form-data-set}
+     */
+    private FormValues build() {
+        for (Element input : form.select("button, input, select, textarea")) {
+            String type = input.attr("type");
+
+            if (input.hasAttr("disabled")) continue;
+            if (input.tagName().equalsIgnoreCase("button") && !type.equals("submit")) continue;
+            if (input.tagName().equalsIgnoreCase("input") && (type.equals("button") || type.equals("reset"))) continue;
+            if (type.equals("checkbox") && input.hasAttr("checked")) continue;
+            if (type.equals("radio") && input.hasAttr("checked")) continue;
+            if (!type.equals("image") && input.attr("name").length() == 0) continue;
+            if (input.parents().is("datalist")) continue;
+
+            if (type.equals("image") || type.equals("file")) continue; // don't support files for now
+            String name = input.attr("name");
+
+            if (input.tagName().equalsIgnoreCase("select")) {
+                for (Element o : input.select("option[selected]")) {
+                    if (o.hasAttr("disabled")) continue;
+                    list.add(name, new BasicNameValuePair(name, o.val()));
+                }
+            } else if (type.equals("checkbox") || type.equals("radio")) {
+                String value = input.hasAttr("value") ? input.val() : "on";
+                list.add(name, new BasicNameValuePair(name, value));
+            } else {
+                list.add(name, new BasicNameValuePair(name, input.val()));
+            }
+        }
+        return this;
+    }
+
+    private FormValues resolve() {
+        for (NameValuePair o : values) {
+            if (list.has(o.getName())) {
+                list.set(o);
+            } else {
+                // for now just set the value even if the form doesn't have a submittable input for the name.
+                // this is to support custom field that generate input dynamically
+                list.set(o);
+            }
+        }
+        return this;
+    }
+
+    public String toString() {
+        return URLEncodedUtils.format(list.flatten(), "UTF-8");
+    }
+
+    public HttpEntity toUrlEncodedEntity() {
+        try {
+            return new UrlEncodedFormEntity(list.flatten(), "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public HttpEntity toMultipartEntity() {
+        MultipartEntityBuilder eb = MultipartEntityBuilder.create();
+        for (NameValuePair p : list.flatten()) {
+            eb.addTextBody(p.getName(), p.getValue());
+        }
+        return eb.build();
+    }
+}
diff --git a/src/main/java/org/apache/sling/hapi/client/forms/Vals.java b/src/main/java/org/apache/sling/hapi/client/forms/Vals.java
new file mode 100644
index 0000000..9e728a6
--- /dev/null
+++ b/src/main/java/org/apache/sling/hapi/client/forms/Vals.java
@@ -0,0 +1,60 @@
+/*******************************************************************************
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.sling.hapi.client.forms;
+
+import org.apache.http.NameValuePair;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Vals {
+    private Map<String, List<NameValuePair>> data = new LinkedHashMap<String, List<NameValuePair>>();
+
+    public void add(String name, NameValuePair pair) {
+        if (data.containsKey(name)) {
+            data.get(name).add(pair);
+        } else {
+            ArrayList<NameValuePair> list = new ArrayList<NameValuePair>();
+            list.add(pair);
+            data.put(name, list);
+        }
+    }
+
+    public boolean has(String name) {
+        return data.containsKey(name);
+    }
+
+    public void set(NameValuePair pair) {
+        ArrayList<NameValuePair> list = new ArrayList<NameValuePair>();
+        list.add(pair);
+        data.put(pair.getName(), list);
+    }
+
+    public List<? extends NameValuePair> flatten() {
+        List<NameValuePair> result = new ArrayList<NameValuePair>();
+
+        for (List<NameValuePair> c : data.values()) {
+            result.addAll(c);
+        }
+
+        return result;
+    }
+}
diff --git a/src/main/java/org/apache/sling/hapi/client/impl/AbstractHtmlClientImpl.java b/src/main/java/org/apache/sling/hapi/client/impl/AbstractHtmlClientImpl.java
new file mode 100644
index 0000000..5c3444a
--- /dev/null
+++ b/src/main/java/org/apache/sling/hapi/client/impl/AbstractHtmlClientImpl.java
@@ -0,0 +1,192 @@
+/*******************************************************************************
+ * 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.sling.hapi.client.impl;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.utils.URIUtils;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.LaxRedirectStrategy;
+import org.apache.http.params.HttpParams;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.util.EntityUtils;
+import org.apache.sling.hapi.client.ClientException;
+import org.apache.sling.hapi.client.Document;
+import org.apache.sling.hapi.client.HtmlClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public abstract class AbstractHtmlClientImpl implements HttpClient, HtmlClient {
+    protected static final Logger LOG = LoggerFactory.getLogger(AbstractHtmlClientImpl.class);
+    protected CloseableHttpClient client;
+    protected URI baseUrl;
+
+    public AbstractHtmlClientImpl(CloseableHttpClient client, String baseUrl) throws URISyntaxException {
+        this.client = client;
+        this.baseUrl = new URI(baseUrl);
+    }
+
+    public AbstractHtmlClientImpl(String baseUrl) throws URISyntaxException {
+        this(HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build(), baseUrl);
+    }
+
+    public AbstractHtmlClientImpl(String baseUrl, String user, String password) throws URISyntaxException {
+        this.baseUrl = new URI(baseUrl);
+        HttpHost targetHost = URIUtils.extractHost(this.baseUrl);
+        BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
+        credsProvider.setCredentials(
+                new AuthScope(targetHost.getHostName(), targetHost.getPort()),
+                new UsernamePasswordCredentials(user, password));
+        this.client = HttpClientBuilder.create()
+                .setDefaultCredentialsProvider(credsProvider)
+                .setRedirectStrategy(new LaxRedirectStrategy())
+                .build();
+    }
+
+
+    @Override
+    public <T extends Document> T enter(String url) throws ClientException {
+        return get(url);
+    }
+
+    @Override
+    public <T extends Document> T get(String url) throws ClientException {
+        try {
+            URI absoluteUri = absoluteUri(url);
+            LOG.info("GET " + absoluteUri);
+            HttpResponse response = this.execute(new HttpGet(absoluteUri));
+            return newDocument(EntityUtils.toString(response.getEntity()));
+
+        } catch (URISyntaxException e) {
+            throw new ClientException("Invalid get url " + url, e);
+        } catch (Exception e) {
+            throw new ClientException("Could not execute GET request", e);
+        }
+    }
+
+    @Override
+    public <T extends Document> T post(String url, HttpEntity entity) throws ClientException {
+        try {
+            URI absoluteUri = absoluteUri(url);
+            LOG.info("POST " + absoluteUri);
+            HttpPost post = new HttpPost(absoluteUri);
+            post.setEntity(entity);
+            HttpResponse response = this.execute(post);
+            return newDocument(EntityUtils.toString(response.getEntity()));
+        } catch (URISyntaxException e) {
+            throw new ClientException("Invalid post url " + url, e);
+        } catch (Exception e) {
+            throw new ClientException("Could not execute POST request", e);
+        }
+    }
+
+    @Override
+    public <T extends Document> T delete(String url) throws ClientException {
+        try {
+            URI absoluteUri = absoluteUri(url);
+            LOG.info("DELETE " + absoluteUri);
+            HttpResponse response = this.execute(new HttpDelete(absoluteUri));
+            return newDocument(response.getEntity().toString());
+        } catch (URISyntaxException e) {
+            throw new ClientException("Invalid post url " + url, e);
+        } catch (Exception e) {
+            throw new ClientException("Could not execute DELETE request", e);
+        }
+    }
+
+    @Override
+    public abstract <T extends Document> T newDocument(String html);
+
+
+    private URI absoluteUri(String url) throws URISyntaxException {
+        URI getUrl = new URI(url);
+        return this.baseUrl.resolve(getUrl);
+    }
+
+    /*
+        Overrides of HttpClient methods
+     */
+    @Override
+    public HttpParams getParams() {
+        return client.getParams();
+    }
+
+    @Override
+    public ClientConnectionManager getConnectionManager() {
+        return client.getConnectionManager();
+    }
+
+    @Override
+    public HttpResponse execute(HttpUriRequest httpUriRequest) throws IOException, ClientProtocolException {
+        return client.execute(httpUriRequest);
+    }
+
+    @Override
+    public HttpResponse execute(HttpUriRequest httpUriRequest, HttpContext httpContext) throws IOException, ClientProtocolException {
+        return client.execute(httpUriRequest, httpContext);
+    }
+
+    @Override
+    public HttpResponse execute(HttpHost httpHost, HttpRequest httpRequest) throws IOException, ClientProtocolException {
+        return client.execute(httpHost, httpRequest);
+    }
+
+    @Override
+    public HttpResponse execute(HttpHost httpHost, HttpRequest httpRequest, HttpContext httpContext) throws IOException, ClientProtocolException {
+        return client.execute(httpHost, httpRequest, httpContext);
+    }
+
+    @Override
+    public <T> T execute(HttpUriRequest httpUriRequest, ResponseHandler<? extends T> responseHandler) throws IOException, ClientProtocolException {
+        return client.execute(httpUriRequest, responseHandler);
+    }
+
+    @Override
+    public <T> T execute(HttpUriRequest httpUriRequest, ResponseHandler<? extends T> responseHandler, HttpContext httpContext) throws IOException, ClientProtocolException {
+        return client.execute(httpUriRequest, responseHandler, httpContext);
+    }
+
+    @Override
+    public <T> T execute(HttpHost httpHost, HttpRequest httpRequest, ResponseHandler<? extends T> responseHandler) throws IOException, ClientProtocolException {
+        return client.execute(httpHost, httpRequest, responseHandler);
+    }
+
+    @Override
+    public <T> T execute(HttpHost httpHost, HttpRequest httpRequest, ResponseHandler<? extends T> responseHandler, HttpContext httpContext) throws IOException, ClientProtocolException {
+        return client.execute(httpHost, httpRequest, responseHandler, httpContext);
+    }
+}
diff --git a/src/main/java/org/apache/sling/hapi/client/microdata/MicrodataDocument.java b/src/main/java/org/apache/sling/hapi/client/microdata/MicrodataDocument.java
new file mode 100644
index 0000000..2f6ee91
--- /dev/null
+++ b/src/main/java/org/apache/sling/hapi/client/microdata/MicrodataDocument.java
@@ -0,0 +1,399 @@
+/*******************************************************************************
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.sling.hapi.client.microdata;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.NameValuePair;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.sling.hapi.client.*;
+import org.apache.sling.hapi.client.forms.FormValues;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.*;
+
+public class MicrodataDocument implements Document {
+    private org.jsoup.nodes.Document jsoupDocument;
+    private HtmlClient client;
+
+    public MicrodataDocument(String html, HtmlClient client, String baseUrl) {
+        this.jsoupDocument = Jsoup.parse(html, baseUrl);
+        this.client = client;
+    }
+
+
+    @Override
+    public Items link(String rel) throws ClientException {
+        Item me = toItem(jsoupDocument);
+        return me.link(rel);
+    }
+
+    @Override
+    public Items form(String rel) throws ClientException {
+        Item me = toItem(jsoupDocument);
+        return me.form(rel);
+    }
+
+    @Override
+    public Items item(String rel) {
+        List<Item> items = new ArrayList<Item>();
+
+        for (Item i : items()) {
+            if (((ItemImpl) i).el.hasClass(rel)) {
+                items.add(i);
+            }
+        }
+
+        return new ItemsImpl(items);
+    }
+
+    @Override
+    public Items items() {
+        return new ItemsImpl(selectItems(jsoupDocument, new ArrayList<Item>()));
+    }
+
+    @Override
+    public String toString() {
+        return this.jsoupDocument.toString();
+    }
+
+    private String toClass(String name) {
+        return "." + name;
+    }
+
+    private List<Item> toItems(Elements el) {
+        List<Item> items = new ArrayList<Item>();
+
+        for (Element e : el) {
+            items.add(toItem(e));
+        }
+
+        return items;
+    }
+
+    private Item toItem(Element el) {
+        return new ItemImpl(el, this);
+    }
+
+    private List<Item> selectItems(Element e, List<Item> items) {
+        if (e.hasAttr("itemscope") && !e.hasAttr("itemprop")) {
+            items.add(new ItemImpl(e, this));
+            return items;
+        }
+
+        for (Element c : e.children()) {
+            selectItems(c, items);
+        }
+
+        return items;
+    }
+
+
+    private class ItemImpl implements Item {
+
+        private Element el;
+        private MicrodataDocument document;
+        private ItemImpl followed;
+
+        public ItemImpl(Element element, MicrodataDocument document) {
+            if (element == null) throw new NullPointerException("element is mandatory");
+            this.el = element;
+            this.document = document;
+        }
+
+        @Override
+        public Items prop(String name) throws ClientException {
+            return new ItemsImpl(selectProps(getProxy().el, name, new ArrayList<Item>()));
+        }
+
+        @Override
+        public Set<String> props() throws ClientException {
+            return selectAllPropNames(getProxy().el, new HashSet<String>());
+        }
+
+        @Override
+        public Items link(String rel) throws ClientException {
+            return new ItemsImpl(toItems(getProxy().el.select("link[rel=" + rel + "], a[rel=" + rel + "]")));
+        }
+
+        @Override
+        public Items form(String rel) throws ClientException {
+            return new ItemsImpl(toItems(getProxy().el.select("form[data-rel=" + rel + "]")));
+        }
+
+
+        /* Private methods */
+
+        private List<Item> selectProps(Element e, String name, List<Item> items) {
+            for (Element c : e.children()) {
+                if (c.hasAttr("itemprop")) {
+                    if (c.attr("itemprop").equals(name)) {
+                        items.add(new ItemImpl(c, document));
+                    }
+
+                    if (c.hasAttr("itemscope")) {
+                        continue;
+                    }
+                }
+
+                selectProps(c, name, items);
+            }
+
+            return items;
+        }
+
+        private Set<String> selectAllPropNames(Element e, Set<String> items) {
+            for (Element c : e.children()) {
+                if (c.hasAttr("itemprop")) {
+                    items.add(c.attr("itemprop"));
+                }
+
+                if (c.hasAttr("itemscope")) {
+                        continue;
+                }
+
+                selectAllPropNames(c, items);
+            }
+            return items;
+        }
+
+        private ItemImpl getProxy() throws ClientException {
+            if (el.tagName().equalsIgnoreCase("a") && el.hasAttr("href")) {
+                if (followed == null) {
+                    followed = follow(el, document);
+                }
+                return followed;
+            }
+
+            return this;
+        }
+
+        private ItemImpl follow(Element el, MicrodataDocument doc) throws ClientException {
+            if (el.hasAttr("itemscope")) {
+                return new ItemImpl(el, doc);
+            }
+
+            if (el.tagName().equalsIgnoreCase("a") && el.hasAttr("href")) {
+                String href = el.attr("href");
+
+                if (href.startsWith("#")) {
+                    Element first = el.ownerDocument().select(href).first();
+                    return first == null ? null : follow(first, doc);
+                }
+
+                String absHref = el.attr("abs:href");
+                MicrodataDocument d = (MicrodataDocument) doc.client.get(absHref);
+
+                try {
+                    URI uri = new URI(absHref);
+
+                    String fragment = uri.getRawFragment();
+                    if (fragment != null) {
+                        Element e = d.jsoupDocument.getElementById(fragment);
+                        return e == null ? null : follow(e, d);
+                    }
+                } catch (URISyntaxException ex) {
+                    throw new ClientException("Error parsing URI: " + absHref, ex);
+                }
+
+                ItemsImpl items = (ItemsImpl) d.items();
+
+                if (items.length() == 1) {
+                    return (ItemImpl) items.at(0);
+                }
+
+                throw new ClientException("Unable determine item: " + absHref);
+            }
+
+            return new ItemImpl(el, doc);
+        }
+
+        @Override
+        public String text() throws ClientException {
+            // resolve element
+            Element el = getProxy().el;
+
+            // if it's a meta, get the value of the content attr
+            if (el.tagName().equalsIgnoreCase("meta") && el.hasAttr("content")) {
+                return el.attr("content");
+            }
+
+            // else, get the text value using jsoup
+            return getProxy().el.text();
+        }
+
+        @Override
+        public boolean bool() throws ClientException {
+            return Boolean.parseBoolean(text());
+        }
+
+        @Override
+        public int number() throws ClientException {
+            return Integer.parseInt(text());
+        }
+
+        @Override
+        public String href() {
+            return el.attr("href");
+        }
+
+        @Override
+        public String src() {
+            return el.attr("src");
+        }
+
+        @Override
+        public Document follow() throws ClientException {
+            if ((el.tagName().equalsIgnoreCase("a") || el.tagName().equalsIgnoreCase("link")) && el.hasAttr("href")) {
+                String href = el.attr("href");
+
+                if (href.startsWith("#")) {
+                    return document;
+                }
+
+                return document.client.enter(el.attr("abs:href"));
+            }
+
+            throw new ClientException("Unable to follow: " + el.toString());
+        }
+
+        @Override
+        public Document submit(Iterable<NameValuePair> values) throws ClientException {
+            if (el.tagName().equalsIgnoreCase("form")) {
+                String action = el.attr("abs:action");
+                if (action.length() == 0) {
+                    action = el.baseUri();
+                }
+
+                String method = el.attr("method");
+
+                if (method.length() == 0 || method.equalsIgnoreCase("get")) {
+                    FormValues query = new FormValues(el, values);
+                    String url = action + (action.contains("?") ? "?" : "&") + query.toString();
+                    return document.client.enter(url);
+                }
+
+                if (method.equalsIgnoreCase("post")) {
+                    String enctype = el.attr("enctype");
+
+                    FormValues v = new FormValues(el, values);
+                    if (enctype.length() == 0 || enctype.equalsIgnoreCase("application/x-www-form-urlencoded")) {
+                        return document.client.post(action, v.toUrlEncodedEntity());
+                    } else if (enctype.equalsIgnoreCase("multipart/form-data")) {
+                        return document.client.post(action, v.toMultipartEntity());
+                    }
+
+                    throw new ClientException("Unsupported form enctype: " + enctype);
+                }
+
+                throw new ClientException("Unsupported form method: " + method);
+            }
+
+            throw new ClientException("The item is not a form");
+        }
+
+
+    }
+
+    /**
+     * Items impl for microdata
+     */
+    private class ItemsImpl implements Items {
+        private List<Item> items;
+
+        public ItemsImpl(List<Item> items) {
+            this.items = items;
+        }
+
+        @Override
+        public Iterator<Item> iterator() {
+            return items.iterator();
+        }
+
+        @Override
+        public Item at(int index) {
+            return items.get(index);
+        }
+
+        @Override
+        public int length() {
+            return items.size();
+        }
+
+        @Override
+        public Items prop(String name) throws ClientException {
+            return items.get(0).prop(name);
+        }
+
+        @Override
+        public Set<String> props() throws ClientException {
+            return items.get(0).props();
+        }
+
+        @Override
+        public Items link(String rel) throws ClientException {
+            return items.get(0).link(rel);
+        }
+
+        @Override
+        public Items form(String rel) throws ClientException {
+            return items.get(0).form(rel);
+        }
+
+        @Override
+        public String text() throws ClientException {
+            return items.get(0).text();
+        }
+
+        @Override
+        public boolean bool() throws ClientException {
+            return items.get(0).bool();
+        }
+
+        @Override
+        public int number() throws ClientException {
+            return items.get(0).number();
+        }
+
+        @Override
+        public String href() {
+            return items.get(0).href();
+        }
+
+        @Override
+        public String src() {
+            return items.get(0).src();
+        }
+
+        @Override
+        public Document follow() throws ClientException {
+            return items.get(0).follow();
+        }
+
+        @Override
+        public Document submit(Iterable<NameValuePair> values) throws ClientException {
+            return items.get(0).submit(values);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/hapi/client/microdata/MicrodataHtmlClient.java b/src/main/java/org/apache/sling/hapi/client/microdata/MicrodataHtmlClient.java
new file mode 100644
index 0000000..807067a
--- /dev/null
+++ b/src/main/java/org/apache/sling/hapi/client/microdata/MicrodataHtmlClient.java
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.sling.hapi.client.microdata;
+
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.sling.hapi.client.impl.AbstractHtmlClientImpl;
+
+import java.net.URISyntaxException;
+
+public class MicrodataHtmlClient extends AbstractHtmlClientImpl {
+
+    public MicrodataHtmlClient(CloseableHttpClient client, String baseUrl) throws URISyntaxException {
+        super(client, baseUrl);
+    }
+
+    public MicrodataHtmlClient(String baseUrl) throws URISyntaxException {
+        super(baseUrl);
+    }
+
+    public MicrodataHtmlClient(String baseUrl, String user, String password) throws URISyntaxException {
+        super(baseUrl, user, password);
+    }
+
+    @Override
+    public MicrodataDocument newDocument(String html) {
+        return new MicrodataDocument(html, this, this.baseUrl.toString());
+    }
+}
diff --git a/src/test/java/FormTest.java b/src/test/java/FormTest.java
new file mode 100644
index 0000000..3c9424d
--- /dev/null
+++ b/src/test/java/FormTest.java
@@ -0,0 +1,114 @@
+/*******************************************************************************
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ ******************************************************************************/
+
+import org.apache.commons.io.IOUtils;
+import org.apache.http.*;
+import org.apache.http.client.utils.URIUtils;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.localserver.LocalServerTestBase;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpRequestHandler;
+import org.apache.sling.hapi.client.ClientException;
+import org.apache.sling.hapi.client.Document;
+import org.apache.sling.hapi.client.Items;
+import org.apache.sling.hapi.client.microdata.MicrodataHtmlClient;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+
+
+public class FormTest extends LocalServerTestBase {
+    public static final String GET_URL = "/test1";
+    public static final String POST_URL = "/testpost1";
+    public static final String OK_RESPONSE = "TEST_OK";
+    public static final String FAIL_RESPONSE = "TEST_FAIL";
+
+    public static String html;
+
+    private HttpHost host;
+    private URI uri;
+
+    @BeforeClass
+    public static void setUpClass() throws IOException {
+        FormTest.html = IOUtils.toString(ItemsTest.class.getResourceAsStream("items_forms.html"), "UTF-8");
+    }
+
+    @Before
+    public void setup() throws Exception {
+        super.setUp();
+        this.serverBootstrap.registerHandler(GET_URL, new HttpRequestHandler() {
+            @Override
+            public void handle(HttpRequest httpRequest, HttpResponse httpResponse, HttpContext httpContext)
+                    throws HttpException, IOException {
+                HttpEntity entity = new StringEntity(html, "UTF-8");
+                httpResponse.setEntity(entity);
+            }
+        }).registerHandler(POST_URL, new HttpRequestHandler() {
+            @Override
+            public void handle(HttpRequest httpRequest, HttpResponse httpResponse, HttpContext httpContext)
+                    throws HttpException, IOException {
+                if (!httpRequest.getRequestLine().getMethod().equals("POST")) {
+                    httpResponse.setEntity(new StringEntity(FAIL_RESPONSE));
+                } else {
+                    httpResponse.setEntity(new StringEntity(OK_RESPONSE));
+                }
+                httpResponse.setStatusCode(302);
+                httpResponse.setHeader("Location", GET_URL);
+            }
+        });
+
+        // start server
+        this.host = this.start();
+        this.uri = URIUtils.rewriteURI(new URI("/"), host);
+    }
+
+    @Test
+    public void testForm() throws ClientException, URISyntaxException {
+        MicrodataHtmlClient client = new MicrodataHtmlClient(uri.toString());
+        Document doc = client.enter(GET_URL);
+        Items items = doc.items();
+        Assert.assertThat(items.length(), equalTo(1));
+        Items form = doc.form("test");
+        Assert.assertThat(form.length(), equalTo(2));
+
+        List<NameValuePair> data = new ArrayList<NameValuePair>();
+        data.add(new BasicNameValuePair("f1", "val1"));
+
+        // url encode enctype
+        Document doc2 = form.at(0).submit(data);
+        Assert.assertThat(doc2.items().length(), equalTo(1));
+
+        // the multipart enctype
+        Document doc3 = form.at(1).submit(data);
+        Assert.assertThat(doc3.items().length(), equalTo(1));
+
+
+
+    }
+}
diff --git a/src/test/java/GetPostTest.java b/src/test/java/GetPostTest.java
new file mode 100644
index 0000000..e45b8b1
--- /dev/null
+++ b/src/test/java/GetPostTest.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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ ******************************************************************************/
+
+import org.apache.http.*;
+import org.apache.http.client.utils.URIUtils;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.localserver.LocalServerTestBase;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpRequestHandler;
+import org.apache.sling.hapi.client.ClientException;
+import org.apache.sling.hapi.client.Document;
+import org.apache.sling.hapi.client.microdata.MicrodataHtmlClient;
+import org.hamcrest.core.StringContains;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import static org.hamcrest.core.StringContains.containsString;
+
+public class GetPostTest extends LocalServerTestBase {
+    public static final String GET_URL = "/test";
+    public static final String GET_AUTH_URL = "/testauth";
+    public static final String OK_RESPONSE = "TEST_OK";
+    public static final String FAIL_RESPONSE = "TEST_FAIL";
+    public static final String USER = "admin";
+    private static final String PASSWORD = "admin";
+    private static final String AUTH_STRING = "Basic YWRtaW46YWRtaW4=";
+    private static final String REDIRECT_URL = "/test_redirect";
+
+
+    private HttpHost host;
+    private URI uri;
+
+    @Before
+    public void setup() throws Exception {
+        super.setUp();
+        this.serverBootstrap.registerHandler(GET_URL, new HttpRequestHandler() {
+            @Override
+            public void handle(HttpRequest httpRequest, HttpResponse httpResponse, HttpContext httpContext)
+                    throws HttpException, IOException {
+                httpResponse.setEntity(new StringEntity(OK_RESPONSE));
+            }
+        }).registerHandler(GET_AUTH_URL, new HttpRequestHandler() {
+            @Override
+            public void handle(HttpRequest httpRequest, HttpResponse httpResponse, HttpContext httpContext)
+                    throws HttpException, IOException {
+                Header[] headers = httpRequest.getHeaders("Authorization");
+                if (null == headers || headers.length == 0 || !headers[0].getValue().equals(AUTH_STRING)) {
+                    httpResponse.setStatusCode(401);
+                    httpResponse.setHeader("WWW-Authenticate",  "Basic realm=\"TEST\"");
+                } else {
+                    httpResponse.setEntity(new StringEntity(OK_RESPONSE));
+                }
+            }
+        }).registerHandler(REDIRECT_URL, new HttpRequestHandler() {
+            @Override
+            public void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException {
+                response.setStatusCode(307);
+                response.setHeader("Location", GET_URL);
+            }
+        });
+
+        // start server
+        this.host = this.start();
+        this.uri = URIUtils.rewriteURI(new URI("/"), host);
+    }
+
+    @Test
+    public void testValidGet() throws ClientException, URISyntaxException {
+        MicrodataHtmlClient client = new MicrodataHtmlClient(uri.toString());
+        Document doc = client.get(GET_URL);
+        Assert.assertThat("GET request failed", doc.toString(), new StringContains(OK_RESPONSE));
+    }
+
+    @Test
+    public void testValidAuthGet() throws ClientException, URISyntaxException {
+        MicrodataHtmlClient client = new MicrodataHtmlClient(uri.toString(), USER, PASSWORD);
+        Document doc = client.get(GET_AUTH_URL);
+        Assert.assertThat("GET request failed with basic auth", doc.toString(), containsString(OK_RESPONSE));
+    }
+
+    @Test
+    public void testRedirect() throws ClientException, URISyntaxException, UnsupportedEncodingException {
+        MicrodataHtmlClient client = new MicrodataHtmlClient(uri.toString(), USER, PASSWORD);
+        Document doc = client.post(REDIRECT_URL, new StringEntity("test"));
+        Assert.assertThat("POST request failed to redirect", doc.toString(), containsString(OK_RESPONSE));
+    }
+
+}
diff --git a/src/test/java/ItemsTest.java b/src/test/java/ItemsTest.java
new file mode 100644
index 0000000..9e0b962
--- /dev/null
+++ b/src/test/java/ItemsTest.java
@@ -0,0 +1,121 @@
+/*******************************************************************************
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ ******************************************************************************/
+
+import org.apache.commons.io.IOUtils;
+import org.apache.http.*;
+import org.apache.http.client.utils.URIUtils;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.localserver.LocalServerTestBase;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpRequestHandler;
+import org.apache.sling.hapi.client.ClientException;
+import org.apache.sling.hapi.client.Document;
+import org.apache.sling.hapi.client.Items;
+import org.apache.sling.hapi.client.microdata.MicrodataHtmlClient;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+
+
+public class ItemsTest extends LocalServerTestBase {
+    public static final String GET_URL = "/test";
+    public static final String GET_LINKS_URL = "/testlinks";
+    public static final String OK_RESPONSE = "TEST_OK";
+    public static final String FAIL_RESPONSE = "TEST_FAIL";
+
+    public static String html;
+    public static String htmlLinks;
+
+    private HttpHost host;
+    private URI uri;
+
+    @BeforeClass
+    public static void setUpClass() throws IOException {
+        ItemsTest.html = IOUtils.toString(ItemsTest.class.getResourceAsStream("items.html"), "UTF-8");
+        ItemsTest.htmlLinks = IOUtils.toString(ItemsTest.class.getResourceAsStream("items_links.html"), "UTF-8");
+    }
+
+    @Before
+    public void setup() throws Exception {
+        super.setUp();
+        this.serverBootstrap.registerHandler(GET_URL, new HttpRequestHandler() {
+            @Override
+            public void handle(HttpRequest httpRequest, HttpResponse httpResponse, HttpContext httpContext)
+                    throws HttpException, IOException {
+                HttpEntity entity = new StringEntity(html, "UTF-8");
+                httpResponse.setEntity(entity);
+            }
+        }).registerHandler(GET_LINKS_URL, new HttpRequestHandler() {
+            @Override
+            public void handle(HttpRequest httpRequest, HttpResponse httpResponse, HttpContext httpContext)
+                    throws HttpException, IOException {
+                HttpEntity entity = new StringEntity(htmlLinks, "UTF-8");
+                httpResponse.setEntity(entity);
+            }
+        });
+
+        // start server
+        this.host = this.start();
+        this.uri = URIUtils.rewriteURI(new URI("/"), host);
+    }
+
+    @Test
+    public void testItems() throws ClientException, URISyntaxException {
+        MicrodataHtmlClient client = new MicrodataHtmlClient(uri.toString());
+        Document doc = client.enter(GET_URL);
+        Items items = doc.items();
+        Assert.assertThat(items.length(), equalTo(2));
+        for (int i=0; i<2; i++) {
+            Assert.assertThat(items.at(i).prop("name").text(), equalTo("Avatar" + i));
+            Assert.assertThat(items.at(i).prop("genre").text(), equalTo("Science fiction" + i));
+            Assert.assertThat(items.at(i).prop("rank").number(), equalTo(i));
+            Assert.assertThat(items.at(i).prop("director").prop("name").text(), equalTo("James Cameron" + i));
+            Assert.assertThat(items.at(i).prop("director").prop("birthDate").text(), equalTo("August 16, 1954 - " + i));
+        }
+    }
+
+    @Test
+    public void testItemsLinks() throws ClientException, URISyntaxException {
+        MicrodataHtmlClient client = new MicrodataHtmlClient(uri.toString());
+        Document doc = client.enter(GET_LINKS_URL);
+        Items items = doc.items();
+        Assert.assertThat(items.length(), equalTo(1));
+        Assert.assertThat(items.prop("name").text(), equalTo("Avatar"));
+        Assert.assertThat(items.prop("genre").text(), equalTo("Science fiction"));
+        Assert.assertThat(items.prop("rank").number(), equalTo(2));
+        Assert.assertThat(items.prop("director").prop("name").text(), equalTo("James Cameron"));
+        Assert.assertThat(items.prop("director").prop("birthDate").text(), equalTo("August 16, 1954"));
+
+        Assert.assertThat(doc.link("test").length(), equalTo(2));
+        Assert.assertThat(doc.items().link("test").length(), equalTo(1));
+        Assert.assertThat(doc.items().prop("director").link("test").length(), equalTo(1));
+
+        Items otherMovies = doc.link("test").follow().items();
+        Assert.assertThat(otherMovies.length(), equalTo(2));
+
+    }
+
+}
diff --git a/src/test/resources/items.html b/src/test/resources/items.html
new file mode 100644
index 0000000..abe5d78
--- /dev/null
+++ b/src/test/resources/items.html
@@ -0,0 +1,24 @@
+<html>
+<head>
+</head>
+</html>
+<body>
+<div itemscope itemtype ="http://schema.org/Movie">
+    <h1 itemprop="name">Avatar0</h1>
+    <div itemprop="director" itemscope itemtype="http://schema.org/Person">
+        Director: <span itemprop="name" itemscope itemtype="text">James Cameron0</span>
+        (born <span itemprop="birthDate" itemscope itemtype="text">August 16, 1954 - 0</span>)
+    </div>
+    <span itemprop="genre" itemscope itemtype="text">Science fiction0</span>
+    <span itemprop="rank" itemscope="true" itemtype="number">0</span>
+</div>
+<div itemscope itemtype ="http://schema.org/Movie">
+    <h1 itemprop="name">Avatar1</h1>
+    <div itemprop="director" itemscope itemtype="http://schema.org/Person">
+        Director: <span itemprop="name">James Cameron1</span> (born <span itemprop="birthDate">August 16, 1954 - 1</span>)
+    </div>
+    <span itemprop="genre">Science fiction1</span>
+    <span itemprop="rank">1</span>
+</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/test/resources/items_forms.html b/src/test/resources/items_forms.html
new file mode 100644
index 0000000..fd14eb1
--- /dev/null
+++ b/src/test/resources/items_forms.html
@@ -0,0 +1,28 @@
+<html>
+<head>
+</head>
+</html>
+<body>
+<div itemscope itemtype ="http://schema.org/Movie">
+    <h1 itemprop="name">Avatar</h1>
+    <div itemprop="director" itemscope itemtype="http://schema.org/Person">
+        Director: <span itemprop="name" itemscope itemtype="text">James Cameron</span>
+        (born <span itemprop="birthDate" itemscope itemtype="text">August 16, 1954</span>)
+        <p>Also directed: <a rel="test" href="/test">test</a></p>
+    </div>
+    <span itemprop="genre" itemscope itemtype="text">Science fiction</span>
+    <span itemprop="rank" itemscope="true" itemtype="number">2</span>
+</div>
+<div>
+    <form data-rel="test" action="/testpost1" method="post">
+        <input type="text" name="f1" value="default1">
+        <textarea name="f2">val2</textarea>
+    </form>
+
+    <form data-rel="test" action="/testpost1" method="post" enctype="multipart/form-data">
+        <input type="text" name="f1" value="default1">
+        <textarea name="f2">val2</textarea>
+    </form>
+</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/test/resources/items_links.html b/src/test/resources/items_links.html
new file mode 100644
index 0000000..1586116
--- /dev/null
+++ b/src/test/resources/items_links.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+</head>
+</html>
+<body>
+<div itemscope itemtype ="http://schema.org/Movie">
+    <h1 itemprop="name">Avatar</h1>
+    <div itemprop="director" itemscope itemtype="http://schema.org/Person">
+        Director: <span itemprop="name" itemscope itemtype="text">James Cameron</span>
+        (born <span itemprop="birthDate" itemscope itemtype="text">August 16, 1954</span>)
+        <p>Also directed: <a rel="test" href="/test">test</a></p>
+    </div>
+    <span itemprop="genre" itemscope itemtype="text">Science fiction</span>
+    <span itemprop="rank" itemscope="true" itemtype="number">2</span>
+</div>
+<div>
+    <a rel="test" href="/test">test</a>
+</div>
+</body>
+</html>
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.