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/10/18 23:20:41 UTC

[sling-org-apache-sling-resourcebuilder] 01/25: SLING-5356 - move resourcebuilder under bundles/extensions

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

rombert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourcebuilder.git

commit a3e05facc03919f2c21a32918bde2b5fc9ddecc7
Author: Bertrand Delacretaz <bd...@apache.org>
AuthorDate: Wed Jan 6 13:59:59 2016 +0000

    SLING-5356 - move resourcebuilder under bundles/extensions
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1723329 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            | 255 ++++++++++++++++
 .../sling/resourcebuilder/api/ResourceBuilder.java | 102 +++++++
 .../resourcebuilder/impl/MapArgsConverter.java     |  43 +++
 .../resourcebuilder/impl/ResourceBuilderImpl.java  | 261 ++++++++++++++++
 .../impl/ResourceBuilderService.java               | 110 +++++++
 .../customizers/RBIT_TeleporterCustomizer.java     |  46 +++
 .../resourcebuilder/impl/MapArgsConverterTest.java |  49 +++
 .../impl/ResourceBuilderImplTest.java              | 335 +++++++++++++++++++++
 .../sling/resourcebuilder/it/FileRetrievalIT.java  |  93 ++++++
 .../resourcebuilder/it/ResourceBuilderIT.java      | 127 ++++++++
 .../sling/resourcebuilder/it/TestEnvironment.java  |  57 ++++
 .../resourcebuilder/test/ResourceAssertions.java   | 136 +++++++++
 src/test/resources/files/models.js                 |   1 +
 src/test/resources/files/myapp.json                |   4 +
 src/test/resources/files/text.html                 |   3 +
 15 files changed, 1622 insertions(+)

diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..427019b
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,255 @@
+<?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/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>26</version>
+        <relativePath/>
+    </parent>
+
+    <artifactId>org.apache.sling.resourcebuilder</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <packaging>bundle</packaging>
+
+    <name>Apache Sling Resource Builder</name>
+    <description>Utilities to create Sling content</description>
+
+    <properties>
+        <!-- Set this to run the server on a specific port
+        <http.port></http.port>
+         -->
+         
+        <!-- Set this to run tests against an existing server instance -->
+        <keepJarRunning>false</keepJarRunning>
+        
+        <!-- 
+            Options for the VM that executes our runnable jar. 
+            Set debugging options here to debug teleported tests.  
+        -->
+        <jar.executor.vm.options>-Xmx512m</jar.executor.vm.options>
+        
+         <!-- Options for the jar to execute. $JAREXEC_SERVER_PORT$ is replaced by the
+            selected port number -->
+        <jar.executor.jar.options>-p $JAREXEC_SERVER_PORT$</jar.executor.jar.options>
+    </properties>
+    <scm>
+        <connection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/commons/resourcebuilder</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/commons/resourcebuilder</developerConnection>
+        <url>https://svn.apache.org/repos/asf/sling/trunk/bundles/commons/resourcebuilder</url>
+    </scm>
+    
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-scr-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>src/test/resources/**</exclude>
+                        <exclude>sling/**</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <!-- Find free ports to run our server -->
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>reserve-server-port</id>
+                        <goals>
+                            <goal>reserve-network-port</goal>
+                        </goals>
+                        <phase>process-resources</phase>
+                        <configuration>
+                            <portNames>
+                                <portName>http.port</portName>
+                            </portNames>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <artifactId>maven-clean-plugin</artifactId>
+                <configuration>
+                    <filesets>
+                        <fileset>
+                            <directory>${basedir}</directory>
+                            <includes>
+                                <!-- sling folder is the workdir of the executable jar that we test -->
+                                <include>sling/**</include>
+                            </includes>
+                        </fileset>
+                    </filesets>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>integration-test</id>
+                        <goals>
+                            <goal>integration-test</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>verify</id>
+                        <goals>
+                            <goal>verify</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <systemPropertyVariables>
+                        <!--  these are the minimal options required for the jar executor, see bundle-with-it module for more -->
+                        <keepJarRunning>${keepJarRunning}</keepJarRunning>
+                        <jar.executor.jar.options>${jar.executor.jar.options}</jar.executor.jar.options>
+                        <jar.executor.vm.options>${jar.executor.vm.options}</jar.executor.vm.options>
+                        <jar.executor.server.port>${http.port}</jar.executor.server.port>
+                        <additional.bundles.path>${project.build.directory}</additional.bundles.path>
+                        <server.ready.path.1>/:script src="system/sling.js"</server.ready.path.1>
+                        <server.ready.path.2>/.explorer.html:href="/libs/sling/explorer/css/explorer.css"</server.ready.path.2>
+                        <server.ready.path.3>/sling-test/sling/sling-test.html:Sling client library tests</server.ready.path.3>
+                        
+                        <!-- Additional bundles to install for testing -->
+                        <sling.additional.bundle.1>org.apache.sling.junit.core</sling.additional.bundle.1>
+                        <sling.additional.bundle.2>${project.artifactId}-${project.version}.jar</sling.additional.bundle.2>
+                    </systemPropertyVariables>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-runnable-jar</id>
+                        <goals>
+                            <goal>copy-dependencies</goal>
+                        </goals>
+                            <phase>process-resources</phase>
+                        <configuration>
+                            <includeArtifactIds>org.apache.sling.launchpad</includeArtifactIds>
+                            <excludeTransitive>true</excludeTransitive>
+                            <overWriteReleases>false</overWriteReleases>
+                            <overWriteSnapshots>false</overWriteSnapshots>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <!-- 
+                            Consider all dependencies as candidates to be installed
+                            as additional bundles. We use system properties to define
+                            which bundles to install in which order.  
+                        -->
+                        <id>copy-additional-bundles</id>
+                        <goals>
+                            <goal>copy-dependencies</goal>
+                        </goals>
+                            <phase>process-resources</phase>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}</outputDirectory>
+                            <excludeTransitive>true</excludeTransitive>
+                            <overWriteReleases>false</overWriteReleases>
+                            <overWriteSnapshots>false</overWriteSnapshots>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+         </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.api</artifactId>
+            <version>2.3.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.mime</artifactId>
+            <version>2.1.2</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.4</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.sling-mock</artifactId>
+            <version>1.6.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.junit.core</artifactId>
+            <version>1.0.14</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.tools</artifactId>
+            <version>1.0.10</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.junit.teleporter</artifactId>
+            <!-- SNAPSHOT required due to SLING-5365 -->
+            <version>1.0.5-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.launchpad</artifactId>
+            <version>8</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/src/main/java/org/apache/sling/resourcebuilder/api/ResourceBuilder.java b/src/main/java/org/apache/sling/resourcebuilder/api/ResourceBuilder.java
new file mode 100644
index 0000000..1a1f965
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourcebuilder/api/ResourceBuilder.java
@@ -0,0 +1,102 @@
+/*
+ * 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.resourcebuilder.api;
+
+import java.io.InputStream;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+
+import aQute.bnd.annotation.ProviderType;
+
+/** Builds Sling Resources using a simple fluent API */
+@ProviderType
+public interface ResourceBuilder {
+    
+    /** Default primary type for resources created by this builder */
+    public static final String DEFAULT_PRIMARY_TYPE = "nt:unstructured";
+    
+    /** Start a ResourceBuilder using the supplied parent resource 
+     *  @return the new builder
+     * */
+    ResourceBuilder forParent(Resource parent);
+    
+    /** Start a ResourceBuilder using the supplied ResourceResolver,
+     *  starting with the root resource as the builder's parent resource. 
+     *  @return the new builder
+     * */
+    ResourceBuilder forResolver(ResourceResolver r);
+    
+    /** Create a Resource, which optionally becomes the current 
+     *  parent Resource. 
+     * @param relativePath The path of the Resource to create, relative to 
+     *          this builder's current parent Resource.
+     * @param properties optional name-value pairs 
+     * @return this builder
+     */
+    ResourceBuilder resource(String relativePath, Object ... properties);
+
+    /** Create a file under the current parent resource
+     * @param filename The name of the created file
+     * @param data The file data
+     * @param mimeType If null, use the Sling MimeTypeService to set the mime type
+     * @param lastModified if < 0, current time is used
+     * @return this builder
+     */
+    ResourceBuilder file(String filename, InputStream data, String mimeType, long lastModified);
+    
+    /** Create a file under the current parent resource. Mime type is set using the 
+     *  Sling MimeTypeService, and last modified is set to current time.
+     * @param filename The name of the created file
+     * @param data The file data
+     * @return this builder
+     */
+    ResourceBuilder file(String filename, InputStream data);
+    
+    /** Commit created resources */
+    ResourceBuilder commit();
+    
+    /** Set the primary type for intermediate resources created
+     *  when the parent of resource being created does not exist.
+     * @param primaryType If null the DEFAULT_PRIMARY_TYPE is used.
+     * @return this builder
+     */
+    ResourceBuilder withIntermediatePrimaryType(String primaryType);
+    
+    /** Set siblings mode (as opposed to hierarchy mode) where creating a resource 
+     *  doesn't change the current parent. Used to create flat structures.
+     *  This is off by default.
+     * @return this builder
+     */
+    ResourceBuilder siblingsMode();
+    
+    /** Set hierarchy mode (as opposed to siblings mode) where creating a resource 
+     *  sets it as the current parent. Used to create tree structures.
+     *  This is on by default.
+     * @return this builder
+     */
+    ResourceBuilder hierarchyMode();
+    
+    /** Return the current parent resource */
+    Resource getCurrentParent();
+    
+    /** Reset the current parent Resource to the original one.
+     *  Also activates hierarchyMode which is the default mode. */ 
+    ResourceBuilder atParent();
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/resourcebuilder/impl/MapArgsConverter.java b/src/main/java/org/apache/sling/resourcebuilder/impl/MapArgsConverter.java
new file mode 100644
index 0000000..433dfb2
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourcebuilder/impl/MapArgsConverter.java
@@ -0,0 +1,43 @@
+/*
+ * 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.resourcebuilder.impl;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Convert arguments which are a list of Object to a Map, used
+ *  to simplify our builder's syntax.
+ */
+public class MapArgsConverter {
+    
+    /** Convert an args list to a Map */
+    public static Map<String, Object> toMap(Object ... args) {
+        if(args.length % 2 != 0) {
+            throw new IllegalArgumentException("args must be an even number of name/values:" + Arrays.asList(args));
+        }
+        final Map<String, Object> result = new HashMap<String, Object>();
+        for(int i=0 ; i < args.length; i+=2) {
+            result.put(args[i].toString(), args[i+1]);
+        }
+        return Collections.unmodifiableMap(result);
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/resourcebuilder/impl/ResourceBuilderImpl.java b/src/main/java/org/apache/sling/resourcebuilder/impl/ResourceBuilderImpl.java
new file mode 100644
index 0000000..48b9be9
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourcebuilder/impl/ResourceBuilderImpl.java
@@ -0,0 +1,261 @@
+/*
+ * 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.resourcebuilder.impl;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.commons.mime.MimeTypeService;
+import org.apache.sling.resourcebuilder.api.ResourceBuilder;
+
+/** ResourceBuilder implementation */
+public class ResourceBuilderImpl implements ResourceBuilder {
+    private final Resource originalParent;
+    private final ResourceResolver resourceResolver;
+    private Resource currentParent;
+    private String intermediatePrimaryType;
+    private boolean hierarchyMode;
+    
+    public static final String JCR_PRIMARYTYPE = "jcr:primaryType";
+    public static final String JCR_MIMETYPE = "jcr:mimeType";
+    public static final String JCR_LASTMODIFIED = "jcr:lastModified";
+    public static final String JCR_DATA = "jcr:data";
+    public static final String JCR_CONTENT = "jcr:content";
+    public static final String NT_RESOURCE = "nt:resource";
+    public static final String NT_FILE = "nt:file";
+    
+    public static final String CANNOT_RESTART =
+            "Cannot reset the parent resource or resource resolver, please create a new "
+            + "builder using the ResourceBuilder service";
+    
+    private final MimeTypeService mimeTypeService;
+    
+    public ResourceBuilderImpl(Resource parent, MimeTypeService mts) {
+        mimeTypeService = mts;
+        if(parent == null) {
+            throw new IllegalArgumentException("Parent resource is null");
+        }
+        originalParent = parent;
+        resourceResolver = originalParent.getResourceResolver();
+        withIntermediatePrimaryType(null);
+        atParent();
+    }
+    
+    @Override
+    public ResourceBuilder forParent(Resource parent) {
+        throw new UnsupportedOperationException(CANNOT_RESTART);
+    }
+    
+    @Override
+    public ResourceBuilder forResolver(ResourceResolver v) {
+        throw new UnsupportedOperationException(CANNOT_RESTART);
+    }
+    
+    @Override
+    public Resource getCurrentParent() {
+        return currentParent;
+    }
+
+    @Override
+    public ResourceBuilder atParent() {
+        currentParent = originalParent;
+        hierarchyMode();
+        return this;
+    }
+    
+    private void checkRelativePath(String relativePath) {
+        if(relativePath.startsWith("/")) {
+            throw new IllegalArgumentException("Path is not relative:" + relativePath);
+        }
+        if(relativePath.contains("..")) {
+            throw new IllegalArgumentException("Path contains invalid pattern '..': " + relativePath);
+        }
+    }
+
+    private String parentPath(String relativePath) {
+        final String parentPath = currentParent.getPath();
+        final String fullPath = 
+            parentPath.endsWith("/")  ? 
+            parentPath + relativePath : 
+            parentPath + "/" + relativePath;
+        return ResourceUtil.getParent(fullPath);
+    }
+    
+    @Override
+    public ResourceBuilder resource(String relativePath, Object... properties) {
+        Resource r = null;
+        checkRelativePath(relativePath);
+        final String parentPath = parentPath(relativePath);
+        final Resource myParent = ensureResourceExists(parentPath);
+        final String fullPath = currentParent.getPath() + "/" + relativePath;
+        
+        try {
+            r = currentParent.getResourceResolver().getResource(fullPath);
+            final Map<String, Object> props = MapArgsConverter.toMap(properties);
+            if(r == null) {
+                r = currentParent.getResourceResolver().create(myParent, 
+                        ResourceUtil.getName(relativePath), props);
+            } else {
+                // Resource exists, set our properties
+                final ModifiableValueMap mvm = r.adaptTo(ModifiableValueMap.class);
+                if(mvm == null) {
+                    throw new IllegalStateException("Cannot modify properties of " + r.getPath());
+                }
+                for(Map.Entry <String, Object> e : props.entrySet()) {
+                    mvm.put(e.getKey(), e.getValue());
+                }
+            }
+        } catch(PersistenceException pex) {
+            throw new RuntimeException(
+                    "PersistenceException while creating Resource " + relativePath 
+                    + " under " + currentParent.getPath(), pex);
+        }
+        
+        if(r == null) {
+            throw new RuntimeException("Failed to get or create resource " + relativePath 
+                    + " under " + currentParent.getPath());
+        } else if(hierarchyMode) {
+            currentParent = r;
+        }
+        return this;
+    }
+    
+    /** Create a Resource at the specified path if none exists yet,
+     *  using the current intermediate primary type. "Stolen" from
+     *  the sling-mock module's ContentBuilder class.
+     *  @param path Resource path
+     *  @return Resource at path (existing or newly created)
+     */
+    protected final Resource ensureResourceExists(String path) {
+        if(path == null || path.length() == 0 || path.equals("/")) {
+            return resourceResolver.getResource("/");
+        }
+        Resource resource = resourceResolver.getResource(path);
+        if (resource != null) {
+            return resource;
+        }
+        String parentPath = ResourceUtil.getParent(path);
+        String name = ResourceUtil.getName(path);
+        Resource parentResource = ensureResourceExists(parentPath);
+        try {
+            resource = resourceResolver.create(
+                    parentResource, 
+                    name, 
+                    MapArgsConverter.toMap(JCR_PRIMARYTYPE, intermediatePrimaryType));
+            return resource;
+        } catch (PersistenceException ex) {
+            throw new RuntimeException("Unable to create intermediate resource at " + path, ex);
+        }
+    }
+    
+    protected String getMimeType(String filename, String userSuppliedMimeType) {
+        if(userSuppliedMimeType != null) {
+            return userSuppliedMimeType;
+        }
+        return mimeTypeService.getMimeType(filename);
+    }
+    
+    protected long getLastModified(long userSuppliedValue) {
+        if(userSuppliedValue < 0) {
+            return System.currentTimeMillis();
+        }
+        return userSuppliedValue;
+    }
+    
+    @Override
+    public ResourceBuilder file(String relativePath, InputStream data, String mimeType, long lastModified) {
+        checkRelativePath(relativePath);
+        final String name = ResourceUtil.getName(relativePath);
+        if(data == null) {
+            throw new IllegalArgumentException("Data is null for file " + name);
+        }
+        
+        Resource file = null;
+        final ResourceResolver resolver = currentParent.getResourceResolver();
+        final String parentPath = parentPath(relativePath);
+        
+        final Resource parent = ensureResourceExists(parentPath);
+        try {
+            final String fullPath = currentParent.getPath() + "/" + name;
+            if(resolver.getResource(fullPath) != null) {
+                throw new IllegalStateException("Resource already exists:" + fullPath);
+            }
+            final Map<String, Object> fileProps = new HashMap<String, Object>();
+            fileProps.put(JCR_PRIMARYTYPE, NT_FILE);
+            file = resolver.create(parent, name, fileProps);
+            
+            final Map<String, Object> contentProps = new HashMap<String, Object>();
+            contentProps.put(JCR_PRIMARYTYPE, NT_RESOURCE);
+            contentProps.put(JCR_MIMETYPE, getMimeType(name, mimeType));
+            contentProps.put(JCR_LASTMODIFIED, getLastModified(lastModified));
+            contentProps.put(JCR_DATA, data);
+            resolver.create(file, JCR_CONTENT, contentProps); 
+        } catch(PersistenceException pex) {
+            throw new RuntimeException("Unable to create file under " + currentParent.getPath(), pex);
+        }
+        
+        if(file == null) {
+            throw new RuntimeException("Unable to get or created file resource " + relativePath + " under " + currentParent.getPath());
+        }
+        if(hierarchyMode) {
+            currentParent = file;
+        }
+        
+        return this;
+    }
+
+    @Override
+    public ResourceBuilder file(String filename, InputStream data) {
+        return file(filename, data, null, -1);
+    }
+
+    @Override
+    public ResourceBuilder withIntermediatePrimaryType(String primaryType) {
+        intermediatePrimaryType = primaryType == null ? DEFAULT_PRIMARY_TYPE : primaryType;
+        return this;
+    }
+
+    @Override
+    public ResourceBuilder siblingsMode() {
+        hierarchyMode = false;
+        return this;
+    }
+
+    @Override
+    public ResourceBuilder hierarchyMode() {
+        hierarchyMode = true;
+        return this;
+    }
+    
+    @Override
+    public ResourceBuilder commit() {
+        try {
+            resourceResolver.commit();
+        } catch (PersistenceException ex) {
+            throw new RuntimeException("Unable to commit", ex);
+        }
+        return this;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/resourcebuilder/impl/ResourceBuilderService.java b/src/main/java/org/apache/sling/resourcebuilder/impl/ResourceBuilderService.java
new file mode 100644
index 0000000..87f68db
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourcebuilder/impl/ResourceBuilderService.java
@@ -0,0 +1,110 @@
+/*
+ * 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.resourcebuilder.impl;
+
+import java.io.InputStream;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.commons.mime.MimeTypeService;
+import org.apache.sling.resourcebuilder.api.ResourceBuilder;
+
+/** ResourceBuilder service, only implements the for* methods to
+ *  create new builders. This allows us to provide a single service
+ *  interface which transparently becomes stateful, by switching from
+ *  this object to the ResourceBuilderImpl.
+ */
+@Component
+@Service(value=ResourceBuilder.class)
+public class ResourceBuilderService implements ResourceBuilder {
+    
+    @Reference
+    private MimeTypeService mimeTypeService;
+    
+    
+    private ResourceBuilder notStarted() {
+        throw new IllegalStateException(
+                "This ResourceBuilder is not started, please use the"
+                + "forParent or forResolver methods to start it."
+         );
+    }
+
+    @Override
+    public ResourceBuilder forParent(Resource parent) {
+        return new ResourceBuilderImpl(parent, mimeTypeService);
+    }
+
+    @Override
+    public ResourceBuilder forResolver(ResourceResolver r) {
+        final Resource root = r.getResource("/");
+        if(root == null) {
+            throw new IllegalStateException("Cannot read root resource");
+        }
+        return forParent(root);
+    }
+
+    @Override
+    public ResourceBuilder resource(String relativePath, Object... properties) {
+        return notStarted();
+    }
+
+    @Override
+    public ResourceBuilder file(String filename, InputStream data, String mimeType, long lastModified) {
+        return notStarted();
+    }
+
+    @Override
+    public ResourceBuilder file(String filename, InputStream data) {
+        return notStarted();
+    }
+
+    @Override
+    public ResourceBuilder commit() {
+        return notStarted();
+    }
+
+    @Override
+    public ResourceBuilder withIntermediatePrimaryType(String primaryType) {
+        return notStarted();
+    }
+
+    @Override
+    public ResourceBuilder siblingsMode() {
+        return notStarted();
+    }
+
+    @Override
+    public ResourceBuilder hierarchyMode() {
+        return notStarted();
+    }
+
+    @Override
+    public Resource getCurrentParent() {
+        notStarted();
+        return null;
+    }
+
+    @Override
+    public ResourceBuilder atParent() {
+        return notStarted();
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/junit/teleporter/customizers/RBIT_TeleporterCustomizer.java b/src/test/java/org/apache/sling/junit/teleporter/customizers/RBIT_TeleporterCustomizer.java
new file mode 100644
index 0000000..765923b
--- /dev/null
+++ b/src/test/java/org/apache/sling/junit/teleporter/customizers/RBIT_TeleporterCustomizer.java
@@ -0,0 +1,46 @@
+/*
+ * 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.junit.teleporter.customizers;
+
+import org.apache.sling.junit.rules.TeleporterRule;
+import org.apache.sling.resourcebuilder.api.ResourceBuilder;
+import org.apache.sling.testing.teleporter.client.ClientSideTeleporter;
+import org.apache.sling.testing.tools.sling.SlingTestBase;
+import org.apache.sling.testing.tools.sling.TimeoutsProvider;
+
+import aQute.bnd.osgi.Constants;
+
+/** Setup the ClientSideTeleporter for our integration tests.
+ */
+public class RBIT_TeleporterCustomizer implements TeleporterRule.Customizer {
+
+    private final static SlingTestBase S = new SlingTestBase();
+    
+    @Override
+    public void customize(TeleporterRule t, String options) {
+        final ClientSideTeleporter cst = (ClientSideTeleporter)t;
+        cst.setBaseUrl(S.getServerBaseUrl());
+        cst.setServerCredentials(S.getServerUsername(), S.getServerPassword());
+        cst.setTestReadyTimeoutSeconds(TimeoutsProvider.getInstance().getTimeout(5));
+        
+        // Make sure our bundle API is imported instead of embedded
+        final String apiPackage = ResourceBuilder.class.getPackage().getName();
+        cst.includeDependencyPrefix("org.apache.sling.resourcebuilder");
+        cst.excludeDependencyPrefix(apiPackage);
+        cst.getAdditionalBundleHeaders().put(Constants.IMPORT_PACKAGE, apiPackage);
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/resourcebuilder/impl/MapArgsConverterTest.java b/src/test/java/org/apache/sling/resourcebuilder/impl/MapArgsConverterTest.java
new file mode 100644
index 0000000..ec1a411
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourcebuilder/impl/MapArgsConverterTest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.resourcebuilder.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.junit.Test;
+
+public class MapArgsConverterTest {
+    
+    @Test
+    public void validArguments() throws PersistenceException {
+        final Map<String, Object> m = MapArgsConverter.toMap("foo", "bar", "count", 21);
+        assertEquals(2, m.size());
+        assertEquals("bar", m.get("foo"));
+        assertEquals(21, m.get("count"));
+    }
+    
+    @Test
+    public void noArguments() throws PersistenceException {
+        final Map<String, Object> m = MapArgsConverter.toMap();
+        assertTrue(m.isEmpty());
+    }
+    
+    @Test(expected=IllegalArgumentException.class)
+    public void barArguments() throws PersistenceException {
+        MapArgsConverter.toMap("foo", "bar", "count");
+    }
+}
diff --git a/src/test/java/org/apache/sling/resourcebuilder/impl/ResourceBuilderImplTest.java b/src/test/java/org/apache/sling/resourcebuilder/impl/ResourceBuilderImplTest.java
new file mode 100644
index 0000000..608a813
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourcebuilder/impl/ResourceBuilderImplTest.java
@@ -0,0 +1,335 @@
+/*
+ * 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.resourcebuilder.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.util.Random;
+import java.util.UUID;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.commons.mime.MimeTypeService;
+import org.apache.sling.resourcebuilder.test.ResourceAssertions;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.testing.mock.sling.services.MockMimeTypeService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class ResourceBuilderImplTest {
+    
+    private String testRootPath;
+    private ResourceResolver resourceResolver;
+    private long lastModified;
+    private Random random = new Random(System.currentTimeMillis());
+    private static final MimeTypeService mimeTypeService = new MockMimeTypeService();
+    private ResourceAssertions A;
+    
+    @Rule
+    public SlingContext context = new SlingContext(ResourceResolverType.RESOURCERESOLVER_MOCK);
+    
+    private Resource getTestRoot(String path) throws PersistenceException {
+        final Resource root = context.resourceResolver().resolve("/");
+        assertNotNull("Expecting non-null root", root);
+        return resourceResolver.create(root, ResourceUtil.getName(path), null);
+    }
+    
+    private ResourceBuilderImpl getBuilder(String path) throws Exception {
+        lastModified = random.nextLong();
+        
+        final Resource parent = getTestRoot(path);
+        final ResourceBuilderImpl result = new ResourceBuilderImpl(parent, mimeTypeService) {
+            @Override
+            protected long getLastModified(long userSuppliedValue) {
+                final long now = System.currentTimeMillis();
+                final long superValue = super.getLastModified(-1);
+                final long maxDelta = 60 * 1000L;
+                if(superValue < now || superValue - now > maxDelta) {
+                    fail("getLastModified does not seem to use current time as its default value");
+                }
+                
+                if(userSuppliedValue >= 0) {
+                    return super.getLastModified(userSuppliedValue);
+                }
+                return lastModified;
+            }
+        };
+        return result;
+    }
+    
+    @Before
+    public void setup() {
+        testRootPath = "/" + UUID.randomUUID().toString();
+        resourceResolver = context.resourceResolver();
+        A = new ResourceAssertions(testRootPath, resourceResolver);
+    }
+    
+    @After
+    public void cleanup() throws PersistenceException {
+        final Resource r = resourceResolver.getResource(testRootPath);
+        if(r != null) {
+            resourceResolver.delete(r);
+            resourceResolver.commit();
+        }
+    }
+    
+    @Test
+    public void basicResource() throws Exception {
+        getBuilder(testRootPath)
+            .resource("child", "title", "foo")
+            .commit();
+        
+        A.assertProperties("child", "title", "foo");
+        assertEquals(A.fullPath("child"), A.assertResource("child").getPath());
+    }
+    
+    @Test
+    public void ensureResourceExists() throws Exception {
+        
+        class MyResourceBuilder extends ResourceBuilderImpl {
+            MyResourceBuilder() {
+                super(resourceResolver.getResource("/"), null);
+            }
+            
+            Resource r(String path) {
+                return ensureResourceExists(path);
+            }
+        };
+        final MyResourceBuilder b = new MyResourceBuilder();
+        
+        assertEquals("/", b.r(null).getPath());
+        assertEquals("/", b.r("").getPath());
+        assertEquals("/", b.r("/").getPath());
+    }
+    
+    @Test
+    public void deepResource() throws Exception {
+        getBuilder(testRootPath)
+            .resource("a/b/c", "title", "foo")
+            .commit();
+        
+        A.assertProperties("a/b/c", "title", "foo");
+        assertEquals(A.fullPath("a/b/c"), A.assertResource("a/b/c").getPath());
+        A.assertResource("a/b");
+        A.assertResource("a");
+    }
+    
+    @Test
+    public void intermediatePrimaryTypes() throws Exception {
+        getBuilder(testRootPath)
+            .resource("a/b/c")
+            .withIntermediatePrimaryType("foo")
+            .resource("d/e")
+            .withIntermediatePrimaryType(null)
+            .resource("f/g")
+            .commit();
+        
+        A.assertProperties("a/b", ResourceBuilderImpl.JCR_PRIMARYTYPE, "nt:unstructured");
+        A.assertProperties("a/b/c/d", ResourceBuilderImpl.JCR_PRIMARYTYPE, "foo");
+        A.assertProperties("a/b/c/d/e/f", ResourceBuilderImpl.JCR_PRIMARYTYPE, "nt:unstructured");
+    }
+    
+    @Test
+    public void resetParent() throws Exception {
+        getBuilder(testRootPath)
+            .resource("a/b/c")
+            .siblingsMode()
+            .resource("one")
+            .resource("two")
+            .atParent()  // also sets hierarchyMode
+            .resource("d/e")
+            .resource("f/g")
+            .siblingsMode()
+            .resource("three")
+            .resource("four")
+            .commit();
+        
+        A.assertResource("a/b/c");
+        A.assertResource("a/b/c/one");
+        A.assertResource("a/b/c/two");
+        A.assertResource("d/e");
+        A.assertResource("d/e/f/g");
+        A.assertResource("d/e/f/g/three");
+        A.assertResource("d/e/f/g/four");
+    }
+    
+    @Test
+    public void noResetParent() throws Exception {
+        getBuilder(testRootPath)
+            .resource("a/b/c")
+            .resource("d/e")
+            .commit();
+        
+        A.assertResource("a/b/c");
+        A.assertResource("a/b/c/d/e");
+    }
+    
+    @Test
+    public void getParent() throws Exception {
+        final Resource parent = getBuilder(testRootPath).getCurrentParent();
+        assertNotNull(parent);
+        assertEquals(testRootPath, parent.getPath());
+    }
+    
+    @Test(expected=RuntimeException.class)
+    public void missingParentFails() throws Exception {
+        new ResourceBuilderImpl(null, null).resource("foo");
+    }
+    
+    @Test(expected=IllegalArgumentException.class)
+    public void absolutePathFails() throws Exception {
+        getBuilder(testRootPath).resource("/absolute");
+    }
+    
+    @Test(expected=IllegalArgumentException.class)
+    public void aboveParentFails() throws Exception {
+        getBuilder(testRootPath).resource("../foo");
+    }
+    
+    @Test(expected=IllegalArgumentException.class)
+    public void aboveParentFailsFile() throws Exception {
+        getBuilder(testRootPath).file("../foo.js", null);
+    }
+    
+    @Test
+    public void simpleTree() throws Exception {
+        getBuilder(testRootPath)
+            .resource("a/b/c", "title", "foo", "count", 21)
+            .siblingsMode()
+            .resource("1")
+            .resource("2")
+            .resource("3")
+            .hierarchyMode()
+            .resource("with")
+            .resource("more/here", "it", "worked")
+            .resource("deepest", "it", "worked")
+            .commit();
+        
+        A.assertProperties("a/b/c", "count", 21, "title", "foo");
+        A.assertProperties("a/b/c/with/more/here", "it", "worked");
+        A.assertResource("a/b/c/with/more/here/deepest");
+        A.assertResource("a/b/c/1");
+        A.assertResource("a/b/c/2");
+        A.assertResource("a/b/c/3");
+    }
+    
+    @Test
+    public void treeWithFiles() throws Exception {
+        getBuilder(testRootPath)
+            .resource("apps/myapp/components/resource")
+            .siblingsMode()
+            .file("models.js", getClass().getResourceAsStream("/files/models.js"), "MT1", 42)
+            .file("text.html", getClass().getResourceAsStream("/files/text.html"), "MT2", 43)
+            .atParent()
+            .file("apps/myapp.json", getClass().getResourceAsStream("/files/myapp.json"), "MT3", 44)
+            .atParent()
+            .resource("apps/content/myapp/resource")
+            .atParent()
+            .resource("apps/content", "title", "foo")
+            .file("myapp.json", getClass().getResourceAsStream("/files/myapp.json"), "MT4", 45)
+            .commit()
+            ;
+        
+        A.assertResource("apps/content/myapp/resource");
+        A.assertResource("apps/myapp/components/resource");
+        A.assertProperties("apps/content", "title", "foo");
+        
+        A.assertFile("apps/myapp/components/resource/models.js", 
+                "MT1", "function someJavascriptFunction()", 42L);
+        A.assertFile("apps/myapp/components/resource/text.html", 
+                "MT2", "This is an html file", 43L);
+        A.assertFile("apps/myapp.json", 
+                "MT3", "\"sling:resourceType\":\"its/resource/type\"", 44L);
+        A.assertFile("apps/content/myapp.json", 
+                "MT4", "\"sling:resourceType\":\"its/resource/type\"", 45L);
+    }
+    
+    @Test
+    public void autoMimetype() throws Exception {
+        getBuilder(testRootPath)
+            .file("models.js", getClass().getResourceAsStream("/files/models.js"), null, 42)
+            .commit()
+            ;
+        A.assertFile("models.js", 
+                "application/javascript", "function someJavascriptFunction()", 42L);
+    }
+    
+    @Test
+    public void autoLastModified() throws Exception {
+        getBuilder(testRootPath)
+            .file("models.js", getClass().getResourceAsStream("/files/models.js"), "MT1", -1)
+            .commit()
+            ;
+        A.assertFile("models.js", 
+                "MT1", "function someJavascriptFunction()", lastModified);
+    }
+    
+    @Test
+    public void autoEverything() throws Exception {
+        getBuilder(testRootPath)
+            .file("a/b/c/models.js", getClass().getResourceAsStream("/files/models.js"))
+            .commit()
+            ;
+        A.assertFile("a/b/c/models.js", 
+                "application/javascript", "function someJavascriptFunction()", lastModified);
+    }
+    
+    @Test(expected=IllegalStateException.class)
+    public void duplicatedFileFails() throws Exception {
+        getBuilder(testRootPath)
+            .siblingsMode()
+            .file("models.js", getClass().getResourceAsStream("/files/models.js"), null, 42)
+            .file("models.js", getClass().getResourceAsStream("/files/models.js"), null, 42)
+            ;
+    }
+    
+    @Test(expected=IllegalArgumentException.class)
+    public void nullDataFails() throws Exception {
+        getBuilder(testRootPath)
+            .file("models.js", null, null, 42)
+            ;
+    }
+    
+    @Test
+    public void forParent() throws PersistenceException {
+        new ResourceBuilderService()
+            .forParent(getTestRoot(testRootPath))
+            .resource("a/b/c")
+            .commit();
+        A.assertResource("a/b/c");
+    }
+    
+    @Test
+    public void forResolver() throws PersistenceException {
+        new ResourceBuilderService()
+            .forResolver(resourceResolver)
+            .resource("d/e/f")
+            .commit();
+        
+        // Resource is created at root in this case
+        A.assertResource("/d/e/f");
+    }
+}
diff --git a/src/test/java/org/apache/sling/resourcebuilder/it/FileRetrievalIT.java b/src/test/java/org/apache/sling/resourcebuilder/it/FileRetrievalIT.java
new file mode 100644
index 0000000..1991c27
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourcebuilder/it/FileRetrievalIT.java
@@ -0,0 +1,93 @@
+/*
+ * 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.resourcebuilder.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.servlet.ServletException;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceMetadata;
+import org.apache.sling.junit.rules.TeleporterRule;
+import org.apache.sling.resourcebuilder.test.ResourceAssertions;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/** Verify that our file structure is correct,
+ *  by creating a file and retrieving it via
+ *  a Sling request. 
+ */
+public class FileRetrievalIT {
+    
+    @Rule
+    public final TeleporterRule teleporter = 
+        TeleporterRule
+        .forClass(getClass(), "RBIT_Teleporter")
+        .withResources("/files/");
+    
+    private TestEnvironment E;
+    private ResourceAssertions A;
+
+    @Before
+    public void setup() throws LoginException, PersistenceException {
+        E = new TestEnvironment(teleporter);
+        A = new ResourceAssertions(E.testRootPath, E.resolver);
+    }
+    
+    @After
+    public void cleanup() throws PersistenceException {
+        E.cleanup();
+    }
+    
+    @Test
+    public void createAndeRtrieveFile() throws IOException, ServletException {
+        final String expected = "yes, it worked";
+        final long startTime = System.currentTimeMillis();
+        final String mimeType = "application/javascript";
+        
+        E.builder
+            .resource("somefolder")
+            .file("the-model.js", getClass().getResourceAsStream("/files/models.js"))
+            .commit();
+        
+        final Resource r = A.assertFile("somefolder/the-model.js", mimeType, expected, -1L);
+        
+        final ResourceMetadata meta = r.getResourceMetadata();
+        assertTrue("Expecting a last modified time >= startTime", meta.getModificationTime() >= startTime);
+        assertEquals("Expecting the correct mime-type", mimeType, meta.getContentType());
+
+        final InputStream is = r.adaptTo(InputStream.class);
+        assertNotNull("Expecting InputStream for file resource " + r.getPath(), is);
+        try {
+            final String content = A.readFully(is);
+            assertTrue("Expecting [" + expected + "] in content", content.contains(expected));
+        } finally {
+            is.close();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/resourcebuilder/it/ResourceBuilderIT.java b/src/test/java/org/apache/sling/resourcebuilder/it/ResourceBuilderIT.java
new file mode 100644
index 0000000..b869e41
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourcebuilder/it/ResourceBuilderIT.java
@@ -0,0 +1,127 @@
+/*
+ * 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.resourcebuilder.it;
+
+import static org.junit.Assert.fail;
+import java.io.IOException;
+import java.util.Comparator;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.junit.rules.TeleporterRule;
+import org.apache.sling.resourcebuilder.test.ResourceAssertions;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/** Server-side integration test for the 
+ *  ResourceBuilder, acquired via the ResourceBuilderProvider
+ */
+public class ResourceBuilderIT {
+    
+    @Rule
+    public final TeleporterRule teleporter = 
+        TeleporterRule
+        .forClass(getClass(), "RBIT_Teleporter")
+        .withResources("/files/");
+    
+    private TestEnvironment E;
+    private ResourceAssertions A;
+
+    @Before
+    public void setup() throws LoginException, PersistenceException {
+        E = new TestEnvironment(teleporter);
+        A = new ResourceAssertions(E.testRootPath, E.resolver);
+    }
+    
+    @After
+    public void cleanup() throws PersistenceException {
+        E.cleanup();
+    }
+    
+    
+    @Test
+    public void simpleResource() {
+        E.builder
+            .resource("foo", "title", E.testRootPath)
+            .commit();
+        A.assertProperties("foo", "title", E.testRootPath);
+    }
+    
+    @Test
+    public void smallTreeWithFile() throws IOException {
+        E.builder
+            .resource("somefolder")
+            .file("the-model.js", getClass().getResourceAsStream("/files/models.js"), "foo", 42L)
+            .commit();
+        
+        A.assertFile("somefolder/the-model.js", "foo", "yes, it worked", 42L);
+    }
+    
+    @Test
+    public void fileAutoValues() throws IOException {
+        final long startTime = System.currentTimeMillis();
+        E.builder
+            .resource("a/b/c")
+            .file("model2.js", getClass().getResourceAsStream("/files/models.js"))
+            .commit();
+        
+        final Comparator<Long> moreThanStartTime = new Comparator<Long>() {
+            @Override
+            public int compare(Long expected, Long fromResource) {
+                if(fromResource >= startTime) {
+                    return 0;
+                }
+                fail("last-modified is not >= than current time:" + fromResource + " < " + startTime);
+                return -1;
+            }
+        };
+        
+        A.assertFile("a/b/c/model2.js", "application/javascript", "yes, it worked", startTime, moreThanStartTime);
+    }
+    
+    @Test
+    public void usingResolver() throws IOException {
+        E.builderService.forResolver(E.resolver).resource("foo/a/b").commit();
+        E.builderService.forResolver(E.resolver).resource("foo/c/d").commit();
+        A.assertResource("/foo/a/b");
+        A.assertResource("/foo/c/d");
+    }
+    
+    @Test(expected=UnsupportedOperationException.class)
+    public void restartFailsA() throws IOException {
+        E.builder.forParent(E.resolver.getResource("/"));
+    }
+    
+    @Test(expected=UnsupportedOperationException.class)
+    public void restartFailsB() throws IOException {
+        E.builder.forResolver(E.resolver);
+    }
+    
+    @Test(expected=IllegalStateException.class)
+    public void notStartedFailsA() throws IOException {
+        E.builderService.resource("foo");
+    }
+    
+    @Test(expected=IllegalStateException.class)
+    public void notStartedFailsB() throws IOException {
+        E.builderService.file("foo", null);
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/resourcebuilder/it/TestEnvironment.java b/src/test/java/org/apache/sling/resourcebuilder/it/TestEnvironment.java
new file mode 100644
index 0000000..4749587
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourcebuilder/it/TestEnvironment.java
@@ -0,0 +1,57 @@
+/*
+ * 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.resourcebuilder.it;
+
+import java.util.UUID;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.junit.rules.TeleporterRule;
+import org.apache.sling.resourcebuilder.api.ResourceBuilder;
+import org.apache.sling.resourcebuilder.test.ResourceAssertions;
+
+class TestEnvironment {
+    
+    final ResourceBuilder builder;
+    final ResourceBuilder builderService;
+    final ResourceResolver resolver;
+    final String testRootPath;
+    final Resource parent;
+    final ResourceAssertions A;
+
+    TestEnvironment(TeleporterRule teleporter) throws LoginException, PersistenceException {
+        testRootPath = getClass().getSimpleName() + "-" + UUID.randomUUID().toString(); 
+        resolver = teleporter.getService(ResourceResolverFactory.class).getAdministrativeResourceResolver(null);
+        final Resource root = resolver.getResource("/");
+        parent = resolver.create(root, testRootPath, null);
+        builderService = teleporter.getService(ResourceBuilder.class); 
+        builder = builderService.forParent(parent);
+        A = new ResourceAssertions(testRootPath, resolver);
+    }
+    
+    void cleanup() throws PersistenceException {
+        if(resolver != null && parent != null) {
+            resolver.delete(parent);
+            resolver.commit();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/resourcebuilder/test/ResourceAssertions.java b/src/test/java/org/apache/sling/resourcebuilder/test/ResourceAssertions.java
new file mode 100644
index 0000000..cea2069
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourcebuilder/test/ResourceAssertions.java
@@ -0,0 +1,136 @@
+/*
+ * 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.resourcebuilder.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Calendar;
+import java.util.Comparator;
+import java.util.Map;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.resourcebuilder.impl.MapArgsConverter;
+import org.apache.sling.resourcebuilder.impl.ResourceBuilderImpl;
+
+/** Utilities for asserting Resources and their properties */
+public class ResourceAssertions {
+    
+    private final ResourceResolver resourceResolver;
+    private final String testRootPath;
+    
+    public ResourceAssertions(String testRootPath, ResourceResolver r) {
+        this.testRootPath = testRootPath;
+        this.resourceResolver = r;
+    }
+    
+    public String fullPath(String path) {
+        return path.startsWith("/") ? path : testRootPath + "/" + path;
+    }
+    
+    public Resource assertResource(String path) {
+        final Resource result =  resourceResolver.resolve(fullPath(path));
+        assertNotNull("Expecting resource to exist:" + path, result);
+        return result;
+    }
+    
+    /** Assert that a file exists and verify its properties. */
+    public Resource assertFile(String path, String mimeType, String expectedContent, Long lastModified) throws IOException {
+        final Comparator<Long> defaultComparator = new Comparator<Long>() {
+            @Override
+            public int compare(Long expected, Long fromResource) {
+                if(expected == -1) {
+                    return 0;
+                }
+                return expected.compareTo(fromResource);
+            }
+        };
+        return assertFile(path, mimeType, expectedContent, lastModified, defaultComparator);
+    }
+    
+    public String readFully(InputStream is) throws IOException {
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        try {
+            IOUtils.copy(is, bos);
+            return new String(bos.toByteArray());
+        } finally {
+            bos.close();
+            is.close();
+        }
+    }
+    
+    /** Assert that a file exists and verify its properties. */
+    public Resource assertFile(String path, String mimeType, String expectedContent, Long lastModified, Comparator<Long> lastModifiedComparator) throws IOException {
+        final Resource r = assertResource(path);
+        assertNotNull("Expecting resource to exist:" + path, r);
+        
+        // Files are stored according to the standard JCR structure
+        final ValueMap fileVm = r.adaptTo(ValueMap.class);
+        assertNotNull("Expecting ValueMap for " + r.getPath(), fileVm);
+        assertEquals("Expecting an nt:file at " + r.getPath(), 
+                ResourceBuilderImpl.NT_FILE, fileVm.get(ResourceBuilderImpl.JCR_PRIMARYTYPE));
+        final Resource jcrContent = r.getChild(ResourceBuilderImpl.JCR_CONTENT);
+        assertNotNull("Expecting subresource:" + ResourceBuilderImpl.JCR_CONTENT, jcrContent);
+        final ValueMap vm = jcrContent.adaptTo(ValueMap.class);
+        assertNotNull("Expecting ValueMap for " + jcrContent.getPath(), vm);
+        assertEquals("Expecting nt:Resource type for " + jcrContent.getPath(), 
+                ResourceBuilderImpl.NT_RESOURCE, vm.get(ResourceBuilderImpl.JCR_PRIMARYTYPE));
+        assertEquals("Expecting the correct mime-type", mimeType, vm.get(ResourceBuilderImpl.JCR_MIMETYPE));
+        assertEquals("Expecting the correct last modified", 
+                0, lastModifiedComparator.compare(lastModified, getLastModified(vm)));
+        
+        final InputStream is = vm.get(ResourceBuilderImpl.JCR_DATA, InputStream.class);
+        assertNotNull("Expecting InputStream property on nt:resource:" + ResourceBuilderImpl.JCR_DATA, is);
+        final String content = readFully(is);
+        assertTrue("Expecting content to contain " + expectedContent, content.contains(expectedContent));
+        
+        return r;
+    }
+    
+    private Long getLastModified(ValueMap vm) {
+        final Object o = vm.get(ResourceBuilderImpl.JCR_LASTMODIFIED);
+        if(o instanceof Long) {
+            return (Long)o;
+        } else if(o instanceof Calendar) {
+            return ((Calendar)o).getTimeInMillis();
+        }
+        throw new IllegalArgumentException("Unexpected type " + o.getClass().getName());
+    }
+    
+    public void assertProperties(String path, Object ...props) {
+        final Map<String, Object> expected = MapArgsConverter.toMap(props);
+        final Resource r = assertResource(path);
+        final ValueMap vm = r.adaptTo(ValueMap.class);
+        for(Map.Entry<String, Object> e : expected.entrySet()) {
+            final Object value = vm.get(e.getKey());
+            assertNotNull("Expecting property " + e.getKey() + " for resource " + r.getPath());
+            assertEquals(
+                    "Expecting value " + e.getValue() 
+                    + " for property " + e.getKey() + " of resource " + r.getPath()
+                    , e.getValue(), value);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/test/resources/files/models.js b/src/test/resources/files/models.js
new file mode 100644
index 0000000..142f8d3
--- /dev/null
+++ b/src/test/resources/files/models.js
@@ -0,0 +1 @@
+function someJavascriptFunction() { return "yes, it worked." }
\ No newline at end of file
diff --git a/src/test/resources/files/myapp.json b/src/test/resources/files/myapp.json
new file mode 100644
index 0000000..bd3edd2
--- /dev/null
+++ b/src/test/resources/files/myapp.json
@@ -0,0 +1,4 @@
+{
+    "jcr:primaryType":"some:NodeType",
+    "sling:resourceType":"its/resource/type"
+}
\ No newline at end of file
diff --git a/src/test/resources/files/text.html b/src/test/resources/files/text.html
new file mode 100644
index 0000000..57dfc39
--- /dev/null
+++ b/src/test/resources/files/text.html
@@ -0,0 +1,3 @@
+<html>
+This is an html file
+</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>.