changeset 822:e236c83449c6

Move web client classes into internal package. Web client classes have been exported for no reason. This patch is basically the following rename: com.redhat.thermostat.web.client => com.redhat.thermostat.web.client.internal It also removes the hard requirement of the web client package from Eclipse. The web client bundle is part of the thermostat core client feature and the bundle should be started via instructions in p2.inf. Starting the bundle is sufficient in order for WebStorage to be properly registered. Reviewed-by: rkennke, ebaron Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2012-December/004407.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Thu, 06 Dec 2012 18:53:24 +0100
parents 2fedc9d961a0
children 0fe0860a96f2
files eclipse/com.redhat.thermostat.eclipse/META-INF/MANIFEST.MF web/client/pom.xml web/client/src/main/java/com/redhat/thermostat/web/client/Activator.java web/client/src/main/java/com/redhat/thermostat/web/client/WebCursor.java web/client/src/main/java/com/redhat/thermostat/web/client/WebStorage.java web/client/src/main/java/com/redhat/thermostat/web/client/WebStorageProvider.java web/client/src/main/java/com/redhat/thermostat/web/client/internal/Activator.java web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebCursor.java web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorageProvider.java web/client/src/test/java/com/redhat/thermostat/web/client/TestObj.java web/client/src/test/java/com/redhat/thermostat/web/client/WebStorageTest.java web/client/src/test/java/com/redhat/thermostat/web/client/internal/TestObj.java web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java
diffstat 14 files changed, 1190 insertions(+), 1192 deletions(-) [+]
line wrap: on
line diff
--- a/eclipse/com.redhat.thermostat.eclipse/META-INF/MANIFEST.MF	Thu Dec 06 18:31:38 2012 +0100
+++ b/eclipse/com.redhat.thermostat.eclipse/META-INF/MANIFEST.MF	Thu Dec 06 18:53:24 2012 +0100
@@ -9,8 +9,8 @@
 Bundle-ActivationPolicy: lazy
 Require-Bundle: org.eclipse.core.runtime,
  org.eclipse.ui
-Import-Package: com.redhat.thermostat.client.core.views,
- com.redhat.thermostat.client.core.controllers,
+Import-Package: com.redhat.thermostat.client.core.controllers,
+ com.redhat.thermostat.client.core.views,
  com.redhat.thermostat.client.locale,
  com.redhat.thermostat.client.ui,
  com.redhat.thermostat.common,
@@ -21,9 +21,7 @@
  com.redhat.thermostat.host.overview.client.locale,
  com.redhat.thermostat.storage.config,
  com.redhat.thermostat.storage.core,
- com.redhat.thermostat.storage.model,
- com.redhat.thermostat.web.client,
- com.redhat.thermostat.web.common
+ com.redhat.thermostat.storage.model
 Export-Package: com.redhat.thermostat.eclipse,
  com.redhat.thermostat.eclipse.internal;x-friends:="com.redhat.thermostat.eclipse.test,com.redhat.thermostat.eclipse.test.ui",
  com.redhat.thermostat.eclipse.internal.controllers;x-friends:="com.redhat.thermostat.eclipse.test,com.redhat.thermostat.eclipse.test.ui",
--- a/web/client/pom.xml	Thu Dec 06 18:31:38 2012 +0100
+++ b/web/client/pom.xml	Thu Dec 06 18:53:24 2012 +0100
@@ -122,10 +122,10 @@
           <instructions>
             <Bundle-SymbolicName>com.redhat.thermostat.web.client</Bundle-SymbolicName>
             <Bundle-Vendor>Red Hat, Inc.</Bundle-Vendor>
-            <Bundle-Activator>com.redhat.thermostat.web.client.Activator</Bundle-Activator>
-            <Export-Package>
-              com.redhat.thermostat.web.client
-            </Export-Package>
+            <Bundle-Activator>com.redhat.thermostat.web.client.internal.Activator</Bundle-Activator>
+            <Private-Package>
+              com.redhat.thermostat.web.client.internal
+            </Private-Package>
             <!-- Do not autogenerate uses clauses in Manifests -->
             <_nouses>true</_nouses>
           </instructions>
--- a/web/client/src/main/java/com/redhat/thermostat/web/client/Activator.java	Thu Dec 06 18:31:38 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-/*
- * Copyright 2012 Red Hat, Inc.
- *
- * This file is part of Thermostat.
- *
- * Thermostat is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published
- * by the Free Software Foundation; either version 2, or (at your
- * option) any later version.
- *
- * Thermostat is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Thermostat; see the file COPYING.  If not see
- * <http://www.gnu.org/licenses/>.
- *
- * Linking this code with other modules is making a combined work
- * based on this code.  Thus, the terms and conditions of the GNU
- * General Public License cover the whole combination.
- *
- * As a special exception, the copyright holders of this code give
- * you permission to link this code with independent modules to
- * produce an executable, regardless of the license terms of these
- * independent modules, and to copy and distribute the resulting
- * executable under terms of your choice, provided that you also
- * meet, for each linked independent module, the terms and conditions
- * of the license of that module.  An independent module is a module
- * which is not derived from or based on this code.  If you modify
- * this code, you may extend this exception to your version of the
- * library, but you are not obligated to do so.  If you do not wish
- * to do so, delete this exception statement from your version.
- */
-
-
-package com.redhat.thermostat.web.client;
-
-import org.osgi.framework.BundleActivator;
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.ServiceRegistration;
-
-import com.redhat.thermostat.storage.core.Storage;
-import com.redhat.thermostat.storage.core.StorageProvider;
-
-public class Activator implements BundleActivator {
-
-    private ServiceRegistration reg;
-    
-    @Override
-    public void start(BundleContext context) throws Exception {
-        WebStorageProvider storage = new WebStorageProvider();
-        this.reg = context.registerService(StorageProvider.class.getName(), storage, null);
-    }
-
-    @Override
-    public void stop(BundleContext context) throws Exception {
-        reg.unregister();
-    }
-
-}
--- a/web/client/src/main/java/com/redhat/thermostat/web/client/WebCursor.java	Thu Dec 06 18:31:38 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-/*
- * Copyright 2012 Red Hat, Inc.
- *
- * This file is part of Thermostat.
- *
- * Thermostat is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published
- * by the Free Software Foundation; either version 2, or (at your
- * option) any later version.
- *
- * Thermostat is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Thermostat; see the file COPYING.  If not see
- * <http://www.gnu.org/licenses/>.
- *
- * Linking this code with other modules is making a combined work
- * based on this code.  Thus, the terms and conditions of the GNU
- * General Public License cover the whole combination.
- *
- * As a special exception, the copyright holders of this code give
- * you permission to link this code with independent modules to
- * produce an executable, regardless of the license terms of these
- * independent modules, and to copy and distribute the resulting
- * executable under terms of your choice, provided that you also
- * meet, for each linked independent module, the terms and conditions
- * of the license of that module.  An independent module is a module
- * which is not derived from or based on this code.  If you modify
- * this code, you may extend this exception to your version of the
- * library, but you are not obligated to do so.  If you do not wish
- * to do so, delete this exception statement from your version.
- */
-
-
-package com.redhat.thermostat.web.client;
-
-import com.redhat.thermostat.storage.core.Cursor;
-import com.redhat.thermostat.storage.model.Pojo;
-
-class WebCursor<T extends Pojo> implements Cursor<T> {
-
-    private T[] data;
-    private int index;
-
-    WebCursor(T[] data) {
-        this.data = data;
-        index = 0;
-    }
-
-    @Override
-    public boolean hasNext() {
-        return index < data.length;
-    }
-
-    @Override
-    public T next() {
-        T result = data[index];
-        index++;
-        return result;
-    }
-
-}
--- a/web/client/src/main/java/com/redhat/thermostat/web/client/WebStorage.java	Thu Dec 06 18:31:38 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,483 +0,0 @@
-/*
- * Copyright 2012 Red Hat, Inc.
- *
- * This file is part of Thermostat.
- *
- * Thermostat is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published
- * by the Free Software Foundation; either version 2, or (at your
- * option) any later version.
- *
- * Thermostat is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Thermostat; see the file COPYING.  If not see
- * <http://www.gnu.org/licenses/>.
- *
- * Linking this code with other modules is making a combined work
- * based on this code.  Thus, the terms and conditions of the GNU
- * General Public License cover the whole combination.
- *
- * As a special exception, the copyright holders of this code give
- * you permission to link this code with independent modules to
- * produce an executable, regardless of the license terms of these
- * independent modules, and to copy and distribute the resulting
- * executable under terms of your choice, provided that you also
- * meet, for each linked independent module, the terms and conditions
- * of the license of that module.  An independent module is a module
- * which is not derived from or based on this code.  If you modify
- * this code, you may extend this exception to your version of the
- * library, but you are not obligated to do so.  If you do not wish
- * to do so, delete this exception statement from your version.
- */
-
-
-package com.redhat.thermostat.web.client;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.Reader;
-import java.lang.reflect.Array;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-
-import org.apache.http.Header;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.NameValuePair;
-import org.apache.http.StatusLine;
-import org.apache.http.auth.AuthScope;
-import org.apache.http.auth.Credentials;
-import org.apache.http.auth.UsernamePasswordCredentials;
-import org.apache.http.client.entity.UrlEncodedFormEntity;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.conn.ClientConnectionManager;
-import org.apache.http.entity.mime.MultipartEntity;
-import org.apache.http.entity.mime.content.InputStreamBody;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
-import org.apache.http.message.BasicNameValuePair;
-import org.apache.http.util.EntityUtils;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.redhat.thermostat.storage.core.Category;
-import com.redhat.thermostat.storage.core.Connection;
-import com.redhat.thermostat.storage.core.Cursor;
-import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Remove;
-import com.redhat.thermostat.storage.core.Storage;
-import com.redhat.thermostat.storage.core.StorageException;
-import com.redhat.thermostat.storage.core.Update;
-import com.redhat.thermostat.storage.model.AgentIdPojo;
-import com.redhat.thermostat.storage.model.Pojo;
-import com.redhat.thermostat.web.common.ThermostatGSONConverter;
-import com.redhat.thermostat.web.common.WebInsert;
-import com.redhat.thermostat.web.common.WebQuery;
-import com.redhat.thermostat.web.common.WebRemove;
-import com.redhat.thermostat.web.common.WebUpdate;
-
-public class WebStorage implements Storage {
-
-    private static class CloseableHttpEntity implements Closeable, HttpEntity {
-
-        private HttpEntity entity;
-
-        CloseableHttpEntity(HttpEntity entity) {
-            this.entity = entity;
-        }
-
-        @Override
-        public void consumeContent() throws IOException {
-            entity.consumeContent();
-        }
-
-        @Override
-        public InputStream getContent() throws IOException, IllegalStateException {
-            return entity.getContent();
-        }
-
-        @Override
-        public Header getContentEncoding() {
-            return entity.getContentEncoding();
-        }
-
-        @Override
-        public long getContentLength() {
-            return entity.getContentLength();
-        }
-
-        @Override
-        public Header getContentType() {
-            return entity.getContentType();
-        }
-
-        @Override
-        public boolean isChunked() {
-            return entity.isChunked();
-        }
-
-        @Override
-        public boolean isRepeatable() {
-            return entity.isRepeatable();
-        }
-
-        @Override
-        public boolean isStreaming() {
-            return entity.isStreaming();
-        }
-
-        @Override
-        public void writeTo(OutputStream out) throws IOException {
-            entity.writeTo(out);
-        }
-
-        @Override
-        public void close() {
-            try {
-                EntityUtils.consume(entity);
-            } catch (IOException ex) {
-                throw new StorageException(ex);
-            }
-        }
-
-    }
-
-    private final class WebConnection extends Connection {
-        WebConnection() {
-            connected = true;
-        }
-        @Override
-        public void disconnect() {
-            connected = false;
-            fireChanged(ConnectionStatus.DISCONNECTED);
-        }
-
-        @Override
-        public void connect() {
-            try {
-                initAuthentication(httpClient);
-                ping();
-                connected = true;
-                fireChanged(ConnectionStatus.CONNECTED);
-            } catch (Exception ex) {
-                fireChanged(ConnectionStatus.FAILED_TO_CONNECT);
-            }
-        }
-
-        @Override
-        public void setUrl(String url) {
-            super.setUrl(url);
-            endpoint = url;
-        }
-
-        @Override
-        public String getUrl() {
-            return endpoint;
-        }
-    }
-
-    private static class WebDataStream extends InputStream {
-
-        private CloseableHttpEntity entity;
-        private InputStream content;
-
-        WebDataStream(CloseableHttpEntity entity) {
-            this.entity = entity;
-            try {
-                content = entity.getContent();
-            } catch (IllegalStateException | IOException e) {
-                throw new StorageException(e);
-            }
-        }
-
-        @Override
-        public void close() throws IOException {
-            content.close();
-            entity.close();
-        }
-
-        @Override
-        public int read() throws IOException {
-            return content.read();
-        }
-
-        @Override
-        public int available() throws IOException {
-            return content.available();
-        }
-
-        @Override
-        public void mark(int readlimit) {
-            content.mark(readlimit);
-        }
-
-        @Override
-        public boolean markSupported() {
-            return content.markSupported();
-        }
-
-        @Override
-        public int read(byte[] b) throws IOException {
-            return content.read(b);
-        }
-
-        @Override
-        public int read(byte[] b, int off, int len) throws IOException {
-            return content.read(b, off, len);
-        }
-
-        @Override
-        public void reset() throws IOException {
-            content.reset();
-        }
-
-        @Override
-        public long skip(long n) throws IOException {
-            return content.skip(n);
-        }
-
-    }
-
-    private String endpoint;
-    private UUID agentId;
-
-    private Map<Category, Integer> categoryIds;
-    private Gson gson;
-    private DefaultHttpClient httpClient;
-    private String username;
-    private String password;
-
-    public WebStorage() {
-        categoryIds = new HashMap<>();
-        gson = new GsonBuilder().registerTypeHierarchyAdapter(Pojo.class, new ThermostatGSONConverter()).create();
-        ClientConnectionManager connManager = new ThreadSafeClientConnManager();
-        DefaultHttpClient client = new DefaultHttpClient(connManager);
-        httpClient = client;
-    }
-
-    private void initAuthentication(DefaultHttpClient client) throws MalformedURLException {
-        if (username != null && password != null) {
-            URL endpointURL = new URL(endpoint);
-            // TODO: Maybe also limit to realm like 'Thermostat Realm' or such?
-            AuthScope scope = new AuthScope(endpointURL.getHost(), endpointURL.getPort());
-            Credentials creds = new UsernamePasswordCredentials(username, password);
-            client.getCredentialsProvider().setCredentials(scope, creds);
-        }
-    }
-
-    private void ping() throws StorageException {
-        post(endpoint + "/ping", (HttpEntity) null).close();
-    }
-
-    private CloseableHttpEntity post(String url, List<NameValuePair> formparams) throws StorageException {
-        try {
-            return postImpl(url, formparams);
-        } catch (IOException ex) {
-            throw new StorageException(ex);
-        }
-    }
-
-    private CloseableHttpEntity postImpl(String url, List<NameValuePair> formparams) throws IOException {
-        HttpEntity entity;
-        if (formparams != null) {
-            entity = new UrlEncodedFormEntity(formparams, "UTF-8");
-        } else {
-            entity = null;
-        }
-        return postImpl(url, entity);
-    }
-
-    
-    private CloseableHttpEntity post(String url, HttpEntity entity) throws StorageException {
-        try {
-            return postImpl(url, entity);
-        } catch (IOException ex) {
-            throw new StorageException(ex);
-        }
-    }
-
-    private CloseableHttpEntity postImpl(String url, HttpEntity entity) throws IOException {
-        HttpPost httpPost = new HttpPost(url);
-        if (entity != null) {
-            httpPost.setEntity(entity);
-        }
-        HttpResponse response = httpClient.execute(httpPost);
-        StatusLine status = response.getStatusLine();
-        if (status.getStatusCode() != 200) {
-            throw new IOException("Server returned status: " + status);
-        }
-
-        return new CloseableHttpEntity(response.getEntity());
-    }
-
-    private static InputStream getContent(HttpEntity entity) {
-        try {
-            return entity.getContent();
-        } catch (IOException ex) {
-            throw new StorageException(ex);
-        }
-    }
-
-    private static Reader getContentAsReader(HttpEntity entity) {
-        InputStream in = getContent(entity);
-        return new InputStreamReader(in);
-    }
-
-    @Override
-    public void registerCategory(Category category) throws StorageException {
-        NameValuePair nameParam = new BasicNameValuePair("name", category.getName());
-        NameValuePair categoryParam = new BasicNameValuePair("category", gson.toJson(category));
-        List<NameValuePair> formparams = Arrays.asList(nameParam, categoryParam);
-        try (CloseableHttpEntity entity = post(endpoint + "/register-category", formparams)) {
-            Reader reader = getContentAsReader(entity);
-            Integer id = gson.fromJson(reader, Integer.class);
-            categoryIds.put(category, id);
-        }
-    }
-
-    @Override
-    public Query createQuery() {
-        return new WebQuery(categoryIds);
-    }
-
-    @Override
-    public Remove createRemove() {
-        return new WebRemove(categoryIds);
-    }
-
-    @Override
-    public WebUpdate createUpdate() {
-        return new WebUpdate(categoryIds);
-    }
-
-    @Override
-    public <T extends Pojo> Cursor<T> findAllPojos(Query query, Class<T> resultClass) throws StorageException {
-        ((WebQuery) query).setResultClassName(resultClass.getName());
-        NameValuePair queryParam = new BasicNameValuePair("query", gson.toJson(query));
-        List<NameValuePair> formparams = Arrays.asList(queryParam);
-        try (CloseableHttpEntity entity = post(endpoint + "/find-all", formparams)) {
-            Reader reader = getContentAsReader(entity);
-            T[] result = (T[]) gson.fromJson(reader, Array.newInstance(resultClass, 0).getClass());
-            return new WebCursor<T>(result);
-        }
-    }
-
-    @Override
-    public <T extends Pojo> T findPojo(Query query, Class<T> resultClass) throws StorageException {
-        ((WebQuery) query).setResultClassName(resultClass.getName());
-        NameValuePair queryParam = new BasicNameValuePair("query", gson.toJson(query));
-        List<NameValuePair> formparams = Arrays.asList(queryParam);
-        try (CloseableHttpEntity entity = post(endpoint + "/find-pojo", formparams)) {
-            Reader reader = getContentAsReader(entity);
-            T result = gson.fromJson(reader, resultClass);
-            return result;
-        }
-    }
-
-    @Override
-    public String getAgentId() {
-        return agentId.toString();
-    }
-
-    @Override
-    public Connection getConnection() {
-        return new WebConnection();
-    }
-
-    @Override
-    public long getCount(Category category) throws StorageException {
-        NameValuePair categoryParam = new BasicNameValuePair("category", gson.toJson(categoryIds.get(category)));
-        List<NameValuePair> formparams = Arrays.asList(categoryParam);
-        try (CloseableHttpEntity entity = post(endpoint + "/get-count", formparams)) {
-            Reader reader = getContentAsReader(entity);
-            long result = gson.fromJson(reader, Long.class);
-            return result;
-        }
-    }
-
-    @Override
-    public InputStream loadFile(String name) throws StorageException {
-        NameValuePair fileParam = new BasicNameValuePair("file", name);
-        List<NameValuePair> formparams = Arrays.asList(fileParam);
-        CloseableHttpEntity entity = post(endpoint + "/load-file", formparams);
-        return new WebDataStream(entity);
-    }
-
-    @Override
-    public void purge() throws StorageException {
-        post(endpoint + "/purge", (HttpEntity) null).close();
-    }
-
-    @Override
-    public void putPojo(Category category, boolean replace, AgentIdPojo pojo) throws StorageException {
-        // TODO: This logic should probably be moved elsewhere. I.e. out of the Storage API.
-        if (pojo.getAgentId() == null) {
-            pojo.setAgentId(getAgentId());
-        }
-
-        int categoryId = categoryIds.get(category);
-        WebInsert insert = new WebInsert(categoryId, replace, pojo.getClass().getName());
-        NameValuePair insertParam = new BasicNameValuePair("insert", gson.toJson(insert));
-        NameValuePair pojoParam = new BasicNameValuePair("pojo", gson.toJson(pojo));
-        List<NameValuePair> formparams = Arrays.asList(insertParam, pojoParam);
-        post(endpoint + "/put-pojo", formparams).close();
-    }
-
-    @Override
-    public void removePojo(Remove remove) throws StorageException {
-        NameValuePair removeParam = new BasicNameValuePair("remove", gson.toJson(remove));
-        List<NameValuePair> formparams = Arrays.asList(removeParam);
-        post(endpoint + "/remove-pojo", formparams).close();
-    }
-
-    @Override
-    public void saveFile(String name, InputStream in) throws StorageException {
-        InputStreamBody body = new InputStreamBody(in, name);
-        MultipartEntity mpEntity = new MultipartEntity();
-        mpEntity.addPart("file", body);
-        post(endpoint + "/save-file", mpEntity).close();
-    }
-
-    @Override
-    public void setAgentId(UUID agentId) {
-        this.agentId = agentId;
-    }
-
-    @Override
-    public void updatePojo(Update update) throws StorageException {
-        WebUpdate webUp = (WebUpdate) update;
-        List<WebUpdate.UpdateValue> updateValues = webUp.getUpdates();
-        List<Object> values = new ArrayList<>(updateValues.size());
-        for (WebUpdate.UpdateValue updateValue : updateValues) {
-            values.add(updateValue.getValue());
-        }
-
-        NameValuePair updateParam = new BasicNameValuePair("update", gson.toJson(update));
-        NameValuePair valuesParam = new BasicNameValuePair("values", gson.toJson(values));
-        List<NameValuePair> formparams = Arrays.asList(updateParam, valuesParam);
-        post(endpoint + "/update-pojo", formparams).close();
-    }
-
-    public void setEndpoint(String endpoint) {
-        this.endpoint = endpoint;
-    }
-
-    public void setAuthConfig(String username, String password) {
-        this.username = username;
-        this.password = password;
-    }
-
-}
--- a/web/client/src/main/java/com/redhat/thermostat/web/client/WebStorageProvider.java	Thu Dec 06 18:31:38 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-package com.redhat.thermostat.web.client;
-
-import com.redhat.thermostat.storage.config.AuthenticationConfiguration;
-import com.redhat.thermostat.storage.config.StartupConfiguration;
-import com.redhat.thermostat.storage.core.Storage;
-import com.redhat.thermostat.storage.core.StorageProvider;
-
-public class WebStorageProvider implements StorageProvider {
-
-    private StartupConfiguration config;
-    
-    @Override
-    public Storage createStorage() {
-        WebStorage storage = new WebStorage();
-        storage.setEndpoint(config.getDBConnectionString());
-        if (config instanceof AuthenticationConfiguration) {
-            AuthenticationConfiguration authConf = (AuthenticationConfiguration) config;
-            storage.setAuthConfig(authConf.getUsername(), authConf.getPassword());
-        }
-        return storage;
-    }
-
-    @Override
-    public void setConfig(StartupConfiguration config) {
-        this.config = config;
-    }
-
-    @Override
-    public boolean canHandleProtocol() {
-        // use http since this might be https at some point
-        return config.getDBConnectionString().startsWith("http");
-    }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/client/src/main/java/com/redhat/thermostat/web/client/internal/Activator.java	Thu Dec 06 18:53:24 2012 +0100
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+
+package com.redhat.thermostat.web.client.internal;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+
+import com.redhat.thermostat.storage.core.StorageProvider;
+
+public class Activator implements BundleActivator {
+
+    private ServiceRegistration reg;
+    
+    @Override
+    public void start(BundleContext context) throws Exception {
+        WebStorageProvider storage = new WebStorageProvider();
+        this.reg = context.registerService(StorageProvider.class.getName(), storage, null);
+    }
+
+    @Override
+    public void stop(BundleContext context) throws Exception {
+        reg.unregister();
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebCursor.java	Thu Dec 06 18:53:24 2012 +0100
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+
+package com.redhat.thermostat.web.client.internal;
+
+import com.redhat.thermostat.storage.core.Cursor;
+import com.redhat.thermostat.storage.model.Pojo;
+
+class WebCursor<T extends Pojo> implements Cursor<T> {
+
+    private T[] data;
+    private int index;
+
+    WebCursor(T[] data) {
+        this.data = data;
+        index = 0;
+    }
+
+    @Override
+    public boolean hasNext() {
+        return index < data.length;
+    }
+
+    @Override
+    public T next() {
+        T result = data[index];
+        index++;
+        return result;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java	Thu Dec 06 18:53:24 2012 +0100
@@ -0,0 +1,483 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+
+package com.redhat.thermostat.web.client.internal;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.lang.reflect.Array;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.StatusLine;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.entity.mime.MultipartEntity;
+import org.apache.http.entity.mime.content.InputStreamBody;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.redhat.thermostat.storage.core.Category;
+import com.redhat.thermostat.storage.core.Connection;
+import com.redhat.thermostat.storage.core.Cursor;
+import com.redhat.thermostat.storage.core.Query;
+import com.redhat.thermostat.storage.core.Remove;
+import com.redhat.thermostat.storage.core.Storage;
+import com.redhat.thermostat.storage.core.StorageException;
+import com.redhat.thermostat.storage.core.Update;
+import com.redhat.thermostat.storage.model.AgentIdPojo;
+import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.web.common.ThermostatGSONConverter;
+import com.redhat.thermostat.web.common.WebInsert;
+import com.redhat.thermostat.web.common.WebQuery;
+import com.redhat.thermostat.web.common.WebRemove;
+import com.redhat.thermostat.web.common.WebUpdate;
+
+public class WebStorage implements Storage {
+
+    private static class CloseableHttpEntity implements Closeable, HttpEntity {
+
+        private HttpEntity entity;
+
+        CloseableHttpEntity(HttpEntity entity) {
+            this.entity = entity;
+        }
+
+        @Override
+        public void consumeContent() throws IOException {
+            entity.consumeContent();
+        }
+
+        @Override
+        public InputStream getContent() throws IOException, IllegalStateException {
+            return entity.getContent();
+        }
+
+        @Override
+        public Header getContentEncoding() {
+            return entity.getContentEncoding();
+        }
+
+        @Override
+        public long getContentLength() {
+            return entity.getContentLength();
+        }
+
+        @Override
+        public Header getContentType() {
+            return entity.getContentType();
+        }
+
+        @Override
+        public boolean isChunked() {
+            return entity.isChunked();
+        }
+
+        @Override
+        public boolean isRepeatable() {
+            return entity.isRepeatable();
+        }
+
+        @Override
+        public boolean isStreaming() {
+            return entity.isStreaming();
+        }
+
+        @Override
+        public void writeTo(OutputStream out) throws IOException {
+            entity.writeTo(out);
+        }
+
+        @Override
+        public void close() {
+            try {
+                EntityUtils.consume(entity);
+            } catch (IOException ex) {
+                throw new StorageException(ex);
+            }
+        }
+
+    }
+
+    private final class WebConnection extends Connection {
+        WebConnection() {
+            connected = true;
+        }
+        @Override
+        public void disconnect() {
+            connected = false;
+            fireChanged(ConnectionStatus.DISCONNECTED);
+        }
+
+        @Override
+        public void connect() {
+            try {
+                initAuthentication(httpClient);
+                ping();
+                connected = true;
+                fireChanged(ConnectionStatus.CONNECTED);
+            } catch (Exception ex) {
+                fireChanged(ConnectionStatus.FAILED_TO_CONNECT);
+            }
+        }
+
+        @Override
+        public void setUrl(String url) {
+            super.setUrl(url);
+            endpoint = url;
+        }
+
+        @Override
+        public String getUrl() {
+            return endpoint;
+        }
+    }
+
+    private static class WebDataStream extends InputStream {
+
+        private CloseableHttpEntity entity;
+        private InputStream content;
+
+        WebDataStream(CloseableHttpEntity entity) {
+            this.entity = entity;
+            try {
+                content = entity.getContent();
+            } catch (IllegalStateException | IOException e) {
+                throw new StorageException(e);
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            content.close();
+            entity.close();
+        }
+
+        @Override
+        public int read() throws IOException {
+            return content.read();
+        }
+
+        @Override
+        public int available() throws IOException {
+            return content.available();
+        }
+
+        @Override
+        public void mark(int readlimit) {
+            content.mark(readlimit);
+        }
+
+        @Override
+        public boolean markSupported() {
+            return content.markSupported();
+        }
+
+        @Override
+        public int read(byte[] b) throws IOException {
+            return content.read(b);
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            return content.read(b, off, len);
+        }
+
+        @Override
+        public void reset() throws IOException {
+            content.reset();
+        }
+
+        @Override
+        public long skip(long n) throws IOException {
+            return content.skip(n);
+        }
+
+    }
+
+    private String endpoint;
+    private UUID agentId;
+
+    private Map<Category, Integer> categoryIds;
+    private Gson gson;
+    private DefaultHttpClient httpClient;
+    private String username;
+    private String password;
+
+    public WebStorage() {
+        categoryIds = new HashMap<>();
+        gson = new GsonBuilder().registerTypeHierarchyAdapter(Pojo.class, new ThermostatGSONConverter()).create();
+        ClientConnectionManager connManager = new ThreadSafeClientConnManager();
+        DefaultHttpClient client = new DefaultHttpClient(connManager);
+        httpClient = client;
+    }
+
+    private void initAuthentication(DefaultHttpClient client) throws MalformedURLException {
+        if (username != null && password != null) {
+            URL endpointURL = new URL(endpoint);
+            // TODO: Maybe also limit to realm like 'Thermostat Realm' or such?
+            AuthScope scope = new AuthScope(endpointURL.getHost(), endpointURL.getPort());
+            Credentials creds = new UsernamePasswordCredentials(username, password);
+            client.getCredentialsProvider().setCredentials(scope, creds);
+        }
+    }
+
+    private void ping() throws StorageException {
+        post(endpoint + "/ping", (HttpEntity) null).close();
+    }
+
+    private CloseableHttpEntity post(String url, List<NameValuePair> formparams) throws StorageException {
+        try {
+            return postImpl(url, formparams);
+        } catch (IOException ex) {
+            throw new StorageException(ex);
+        }
+    }
+
+    private CloseableHttpEntity postImpl(String url, List<NameValuePair> formparams) throws IOException {
+        HttpEntity entity;
+        if (formparams != null) {
+            entity = new UrlEncodedFormEntity(formparams, "UTF-8");
+        } else {
+            entity = null;
+        }
+        return postImpl(url, entity);
+    }
+
+    
+    private CloseableHttpEntity post(String url, HttpEntity entity) throws StorageException {
+        try {
+            return postImpl(url, entity);
+        } catch (IOException ex) {
+            throw new StorageException(ex);
+        }
+    }
+
+    private CloseableHttpEntity postImpl(String url, HttpEntity entity) throws IOException {
+        HttpPost httpPost = new HttpPost(url);
+        if (entity != null) {
+            httpPost.setEntity(entity);
+        }
+        HttpResponse response = httpClient.execute(httpPost);
+        StatusLine status = response.getStatusLine();
+        if (status.getStatusCode() != 200) {
+            throw new IOException("Server returned status: " + status);
+        }
+
+        return new CloseableHttpEntity(response.getEntity());
+    }
+
+    private static InputStream getContent(HttpEntity entity) {
+        try {
+            return entity.getContent();
+        } catch (IOException ex) {
+            throw new StorageException(ex);
+        }
+    }
+
+    private static Reader getContentAsReader(HttpEntity entity) {
+        InputStream in = getContent(entity);
+        return new InputStreamReader(in);
+    }
+
+    @Override
+    public void registerCategory(Category category) throws StorageException {
+        NameValuePair nameParam = new BasicNameValuePair("name", category.getName());
+        NameValuePair categoryParam = new BasicNameValuePair("category", gson.toJson(category));
+        List<NameValuePair> formparams = Arrays.asList(nameParam, categoryParam);
+        try (CloseableHttpEntity entity = post(endpoint + "/register-category", formparams)) {
+            Reader reader = getContentAsReader(entity);
+            Integer id = gson.fromJson(reader, Integer.class);
+            categoryIds.put(category, id);
+        }
+    }
+
+    @Override
+    public Query createQuery() {
+        return new WebQuery(categoryIds);
+    }
+
+    @Override
+    public Remove createRemove() {
+        return new WebRemove(categoryIds);
+    }
+
+    @Override
+    public WebUpdate createUpdate() {
+        return new WebUpdate(categoryIds);
+    }
+
+    @Override
+    public <T extends Pojo> Cursor<T> findAllPojos(Query query, Class<T> resultClass) throws StorageException {
+        ((WebQuery) query).setResultClassName(resultClass.getName());
+        NameValuePair queryParam = new BasicNameValuePair("query", gson.toJson(query));
+        List<NameValuePair> formparams = Arrays.asList(queryParam);
+        try (CloseableHttpEntity entity = post(endpoint + "/find-all", formparams)) {
+            Reader reader = getContentAsReader(entity);
+            T[] result = (T[]) gson.fromJson(reader, Array.newInstance(resultClass, 0).getClass());
+            return new WebCursor<T>(result);
+        }
+    }
+
+    @Override
+    public <T extends Pojo> T findPojo(Query query, Class<T> resultClass) throws StorageException {
+        ((WebQuery) query).setResultClassName(resultClass.getName());
+        NameValuePair queryParam = new BasicNameValuePair("query", gson.toJson(query));
+        List<NameValuePair> formparams = Arrays.asList(queryParam);
+        try (CloseableHttpEntity entity = post(endpoint + "/find-pojo", formparams)) {
+            Reader reader = getContentAsReader(entity);
+            T result = gson.fromJson(reader, resultClass);
+            return result;
+        }
+    }
+
+    @Override
+    public String getAgentId() {
+        return agentId.toString();
+    }
+
+    @Override
+    public Connection getConnection() {
+        return new WebConnection();
+    }
+
+    @Override
+    public long getCount(Category category) throws StorageException {
+        NameValuePair categoryParam = new BasicNameValuePair("category", gson.toJson(categoryIds.get(category)));
+        List<NameValuePair> formparams = Arrays.asList(categoryParam);
+        try (CloseableHttpEntity entity = post(endpoint + "/get-count", formparams)) {
+            Reader reader = getContentAsReader(entity);
+            long result = gson.fromJson(reader, Long.class);
+            return result;
+        }
+    }
+
+    @Override
+    public InputStream loadFile(String name) throws StorageException {
+        NameValuePair fileParam = new BasicNameValuePair("file", name);
+        List<NameValuePair> formparams = Arrays.asList(fileParam);
+        CloseableHttpEntity entity = post(endpoint + "/load-file", formparams);
+        return new WebDataStream(entity);
+    }
+
+    @Override
+    public void purge() throws StorageException {
+        post(endpoint + "/purge", (HttpEntity) null).close();
+    }
+
+    @Override
+    public void putPojo(Category category, boolean replace, AgentIdPojo pojo) throws StorageException {
+        // TODO: This logic should probably be moved elsewhere. I.e. out of the Storage API.
+        if (pojo.getAgentId() == null) {
+            pojo.setAgentId(getAgentId());
+        }
+
+        int categoryId = categoryIds.get(category);
+        WebInsert insert = new WebInsert(categoryId, replace, pojo.getClass().getName());
+        NameValuePair insertParam = new BasicNameValuePair("insert", gson.toJson(insert));
+        NameValuePair pojoParam = new BasicNameValuePair("pojo", gson.toJson(pojo));
+        List<NameValuePair> formparams = Arrays.asList(insertParam, pojoParam);
+        post(endpoint + "/put-pojo", formparams).close();
+    }
+
+    @Override
+    public void removePojo(Remove remove) throws StorageException {
+        NameValuePair removeParam = new BasicNameValuePair("remove", gson.toJson(remove));
+        List<NameValuePair> formparams = Arrays.asList(removeParam);
+        post(endpoint + "/remove-pojo", formparams).close();
+    }
+
+    @Override
+    public void saveFile(String name, InputStream in) throws StorageException {
+        InputStreamBody body = new InputStreamBody(in, name);
+        MultipartEntity mpEntity = new MultipartEntity();
+        mpEntity.addPart("file", body);
+        post(endpoint + "/save-file", mpEntity).close();
+    }
+
+    @Override
+    public void setAgentId(UUID agentId) {
+        this.agentId = agentId;
+    }
+
+    @Override
+    public void updatePojo(Update update) throws StorageException {
+        WebUpdate webUp = (WebUpdate) update;
+        List<WebUpdate.UpdateValue> updateValues = webUp.getUpdates();
+        List<Object> values = new ArrayList<>(updateValues.size());
+        for (WebUpdate.UpdateValue updateValue : updateValues) {
+            values.add(updateValue.getValue());
+        }
+
+        NameValuePair updateParam = new BasicNameValuePair("update", gson.toJson(update));
+        NameValuePair valuesParam = new BasicNameValuePair("values", gson.toJson(values));
+        List<NameValuePair> formparams = Arrays.asList(updateParam, valuesParam);
+        post(endpoint + "/update-pojo", formparams).close();
+    }
+
+    public void setEndpoint(String endpoint) {
+        this.endpoint = endpoint;
+    }
+
+    public void setAuthConfig(String username, String password) {
+        this.username = username;
+        this.password = password;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorageProvider.java	Thu Dec 06 18:53:24 2012 +0100
@@ -0,0 +1,34 @@
+package com.redhat.thermostat.web.client.internal;
+
+import com.redhat.thermostat.storage.config.AuthenticationConfiguration;
+import com.redhat.thermostat.storage.config.StartupConfiguration;
+import com.redhat.thermostat.storage.core.Storage;
+import com.redhat.thermostat.storage.core.StorageProvider;
+
+public class WebStorageProvider implements StorageProvider {
+
+    private StartupConfiguration config;
+    
+    @Override
+    public Storage createStorage() {
+        WebStorage storage = new WebStorage();
+        storage.setEndpoint(config.getDBConnectionString());
+        if (config instanceof AuthenticationConfiguration) {
+            AuthenticationConfiguration authConf = (AuthenticationConfiguration) config;
+            storage.setAuthConfig(authConf.getUsername(), authConf.getPassword());
+        }
+        return storage;
+    }
+
+    @Override
+    public void setConfig(StartupConfiguration config) {
+        this.config = config;
+    }
+
+    @Override
+    public boolean canHandleProtocol() {
+        // use http since this might be https at some point
+        return config.getDBConnectionString().startsWith("http");
+    }
+
+}
--- a/web/client/src/test/java/com/redhat/thermostat/web/client/TestObj.java	Thu Dec 06 18:31:38 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +0,0 @@
-/*
- * Copyright 2012 Red Hat, Inc.
- *
- * This file is part of Thermostat.
- *
- * Thermostat is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published
- * by the Free Software Foundation; either version 2, or (at your
- * option) any later version.
- *
- * Thermostat is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Thermostat; see the file COPYING.  If not see
- * <http://www.gnu.org/licenses/>.
- *
- * Linking this code with other modules is making a combined work
- * based on this code.  Thus, the terms and conditions of the GNU
- * General Public License cover the whole combination.
- *
- * As a special exception, the copyright holders of this code give
- * you permission to link this code with independent modules to
- * produce an executable, regardless of the license terms of these
- * independent modules, and to copy and distribute the resulting
- * executable under terms of your choice, provided that you also
- * meet, for each linked independent module, the terms and conditions
- * of the license of that module.  An independent module is a module
- * which is not derived from or based on this code.  If you modify
- * this code, you may extend this exception to your version of the
- * library, but you are not obligated to do so.  If you do not wish
- * to do so, delete this exception statement from your version.
- */
-
-
-package com.redhat.thermostat.web.client;
-
-import com.redhat.thermostat.storage.core.Entity;
-import com.redhat.thermostat.storage.core.Persist;
-import com.redhat.thermostat.storage.model.BasePojo;
-
-@Entity
-public class TestObj extends BasePojo {
-
-    
-    private String property1;
-
-    @Persist
-    public void setProperty1(String property1) {
-        this.property1 = property1;
-    }
-
-    @Persist
-    public String getProperty1() {
-        return property1;
-    }
-
-    public boolean equals(Object o) {
-        if (! (o instanceof TestObj)) {
-            return false;
-        }
-        TestObj other = (TestObj) o;
-        return property1.equals(other.property1);
-    }
-}
--- a/web/client/src/test/java/com/redhat/thermostat/web/client/WebStorageTest.java	Thu Dec 06 18:31:38 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,472 +0,0 @@
-/*
- * Copyright 2012 Red Hat, Inc.
- *
- * This file is part of Thermostat.
- *
- * Thermostat is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published
- * by the Free Software Foundation; either version 2, or (at your
- * option) any later version.
- *
- * Thermostat is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Thermostat; see the file COPYING.  If not see
- * <http://www.gnu.org/licenses/>.
- *
- * Linking this code with other modules is making a combined work
- * based on this code.  Thus, the terms and conditions of the GNU
- * General Public License cover the whole combination.
- *
- * As a special exception, the copyright holders of this code give
- * you permission to link this code with independent modules to
- * produce an executable, regardless of the license terms of these
- * independent modules, and to copy and distribute the resulting
- * executable under terms of your choice, provided that you also
- * meet, for each linked independent module, the terms and conditions
- * of the license of that module.  An independent module is a module
- * which is not derived from or based on this code.  If you modify
- * this code, you may extend this exception to your version of the
- * library, but you are not obligated to do so.  If you do not wish
- * to do so, delete this exception statement from your version.
- */
-
-
-package com.redhat.thermostat.web.client;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Reader;
-import java.io.StringReader;
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-import java.util.Arrays;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.eclipse.jetty.server.Request;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.handler.AbstractHandler;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonParser;
-import com.google.gson.JsonSyntaxException;
-import com.redhat.thermostat.storage.core.Categories;
-import com.redhat.thermostat.storage.core.Category;
-import com.redhat.thermostat.storage.core.Cursor;
-import com.redhat.thermostat.storage.core.Key;
-import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Remove;
-import com.redhat.thermostat.storage.core.Query.Criteria;
-import com.redhat.thermostat.test.FreePortFinder;
-import com.redhat.thermostat.test.FreePortFinder.TryPort;
-import com.redhat.thermostat.web.common.Qualifier;
-import com.redhat.thermostat.web.common.WebQuery;
-import com.redhat.thermostat.web.common.WebInsert;
-import com.redhat.thermostat.web.common.WebRemove;
-import com.redhat.thermostat.web.common.WebUpdate;
-
-public class WebStorageTest {
-
-    private Server server;
-
-    private int port;
-
-    private String requestBody;
-
-    private String responseBody;
-    private Map<String,String> headers;
-    private String method;
-    private String requestURI;
-
-    private static Category category;
-    private static Key<String> key1;
-    private static Key<Integer> key2;
-
-    private WebStorage storage;
-
-    @BeforeClass
-    public static void setupCategory() {
-        key1 = new Key<>("property1", true);
-        key2 = new Key<>("property2", true);
-        category = new Category("test", key1);
-    }
-
-    @AfterClass
-    public static void cleanupCategory() {
-        Categories.remove(category);
-        category = null;
-        key1 = null;
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        
-        port = FreePortFinder.findFreePort(new TryPort() {
-            @Override
-            public void tryPort(int port) throws Exception {
-                startServer(port);
-            }
-        });
-
-        storage = new WebStorage();
-        storage.setEndpoint("http://localhost:" + port + "/");
-        storage.setAgentId(new UUID(123, 456));
-        headers = new HashMap<>();
-        requestURI = null;
-        method = null;
-        registerCategory();
-    }
-
-    private void startServer(int port) throws Exception {
-        server = new Server(port);
-        server.setHandler(new AbstractHandler() {
-            
-            @Override
-            public void handle(String target, Request baseRequest,
-                    HttpServletRequest request, HttpServletResponse response)
-                    throws IOException, ServletException {
-                Enumeration<String> headerNames = request.getHeaderNames();
-                while (headerNames.hasMoreElements()) {
-                    String headerName = headerNames.nextElement();
-                    headers.put(headerName, request.getHeader(headerName));
-                }
-
-                method = request.getMethod();
-                requestURI = request.getRequestURI();
-
-                // Read request body.
-                StringBuilder body = new StringBuilder();
-                Reader reader = request.getReader();
-                while (true) {
-                    int read = reader.read();
-                    if (read == -1) {
-                        break;
-                    }
-                    body.append((char) read);
-                }
-                requestBody = body.toString();
-                // Send response body.
-                response.setStatus(HttpServletResponse.SC_OK);
-                if (responseBody != null) {
-                    response.getWriter().write(responseBody);
-                }
-                baseRequest.setHandled(true);
-            }
-        });
-        server.start();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-
-        headers = null;
-        requestURI = null;
-        method = null;
-        storage = null;
-
-        server.stop();
-        server.join();
-    }
-
-    private void registerCategory() {
-
-        // Return 42 for categoryId.
-        Gson gson = new Gson();
-        responseBody = gson.toJson(42);
-
-        storage.registerCategory(category);
-    }
-
-    @Test
-    public void testFindPojo() throws UnsupportedEncodingException, IOException {
-
-        TestObj obj = new TestObj();
-        obj.setProperty1("fluffor");
-        Gson gson = new Gson();
-        responseBody = gson.toJson(obj);
-
-        Query query = storage.createQuery().from(category).where(key1, Criteria.EQUALS, "fluff");
-
-        TestObj result = storage.findPojo(query, TestObj.class);
-        StringReader reader = new StringReader(requestBody);
-        BufferedReader bufRead = new BufferedReader(reader);
-        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
-        String[] parts = line.split("=");
-        assertEquals("query", parts[0]);
-        WebQuery restQuery = gson.fromJson(parts[1], WebQuery.class);
-
-        assertEquals(42, restQuery.getCategoryId());
-        List<Qualifier<?>> qualifiers = restQuery.getQualifiers();
-        assertEquals(1, qualifiers.size());
-        Qualifier<?> qual = qualifiers.get(0);
-        assertEquals(new Key<String>("property1", true), qual.getKey());
-        assertEquals(Criteria.EQUALS, qual.getCriteria());
-        assertEquals("fluff", qual.getValue());
-
-        assertEquals("fluffor", result.getProperty1());
-    }
-
-    @Test
-    public void testFindAllPojos() throws UnsupportedEncodingException, IOException {
-
-        TestObj obj1 = new TestObj();
-        obj1.setProperty1("fluffor1");
-        TestObj obj2 = new TestObj();
-        obj2.setProperty1("fluffor2");
-        Gson gson = new Gson();
-        responseBody = gson.toJson(Arrays.asList(obj1, obj2));
-
-        Key<String> key1 = new Key<>("property1", true);
-        Query query = storage.createQuery().from(category).where(key1, Criteria.EQUALS, "fluff");
-
-        Cursor<TestObj> results = storage.findAllPojos(query, TestObj.class);
-        StringReader reader = new StringReader(requestBody);
-        BufferedReader bufRead = new BufferedReader(reader);
-        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
-        String[] parts = line.split("=");
-        assertEquals("query", parts[0]);
-        WebQuery restQuery = gson.fromJson(parts[1], WebQuery.class);
-
-        assertEquals(42, restQuery.getCategoryId());
-        List<Qualifier<?>> qualifiers = restQuery.getQualifiers();
-        assertEquals(1, qualifiers.size());
-        Qualifier<?> qual = qualifiers.get(0);
-        assertEquals(new Key<String>("property1", true), qual.getKey());
-        assertEquals(Criteria.EQUALS, qual.getCriteria());
-        assertEquals("fluff", qual.getValue());
-
-        assertTrue(results.hasNext());
-        assertEquals("fluffor1", results.next().getProperty1());
-        assertTrue(results.hasNext());
-        assertEquals("fluffor2", results.next().getProperty1());
-        assertFalse(results.hasNext());
-    }
-
-    @Test
-    public void testPut() throws IOException, JsonSyntaxException, ClassNotFoundException {
-
-        TestObj obj = new TestObj();
-        obj.setProperty1("fluff");
-
-        storage.putPojo(category, true, obj);
-
-        Gson gson = new Gson();
-        StringReader reader = new StringReader(requestBody);
-        BufferedReader bufRead = new BufferedReader(reader);
-        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
-        String [] params = line.split("&");
-        assertEquals(2, params.length);
-        String[] parts = params[0].split("=");
-        assertEquals("insert", parts[0]);
-        WebInsert insert = gson.fromJson(parts[1], WebInsert.class);
-        assertEquals(42, insert.getCategoryId());
-        assertEquals(true, insert.isReplace());
-        assertEquals(TestObj.class.getName(), insert.getPojoClass());
-
-        parts = params[1].split("=");
-        assertEquals(2, parts.length);
-        assertEquals("pojo", parts[0]);
-        Object resultObj = gson.fromJson(parts[1], Class.forName(insert.getPojoClass()));
-        assertEquals(obj, resultObj);
-    }
-
-    @Test
-    public void testCreateRemove() {
-        WebRemove remove = (WebRemove) storage.createRemove();
-        assertNotNull(remove);
-        remove = remove.from(category);
-        assertEquals(42, remove.getCategoryId());
-        assertNotNull(remove);
-        remove = remove.where(key1, "test");
-        assertNotNull(remove);
-        List<Qualifier<?>> qualifiers = remove.getQualifiers();
-        assertEquals(1, qualifiers.size());
-        Qualifier<?> qualifier = qualifiers.get(0);
-        assertEquals(key1, qualifier.getKey());
-        assertEquals(Criteria.EQUALS, qualifier.getCriteria());
-        assertEquals("test", qualifier.getValue());
-    }
-
-    @Test
-    public void testRemovePojo() throws UnsupportedEncodingException, IOException {
-        Remove remove = storage.createRemove().from(category).where(key1, "test");
-        storage.removePojo(remove);
-
-        Gson gson = new Gson();
-        StringReader reader = new StringReader(requestBody);
-        BufferedReader bufRead = new BufferedReader(reader);
-        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
-        System.err.println("line: " + line);
-        String[] parts = line.split("=");
-        assertEquals("remove", parts[0]);
-        WebRemove actualRemove = gson.fromJson(parts[1], WebRemove.class);
-        
-        assertEquals(42, actualRemove.getCategoryId());
-        List<Qualifier<?>> qualifiers = actualRemove.getQualifiers();
-        assertEquals(1, qualifiers.size());
-        Qualifier<?> qualifier = qualifiers.get(0);
-        assertEquals(key1, qualifier.getKey());
-        assertEquals(Criteria.EQUALS, qualifier.getCriteria());
-        assertEquals("test", qualifier.getValue());
-    }
-
-    @Test
-    public void testCreateUpdate() {
-        WebUpdate update = (WebUpdate) storage.createUpdate();
-        assertNotNull(update);
-        update = update.from(category);
-        assertEquals(42, update.getCategoryId());
-        assertNotNull(update);
-        update = update.where(key1, "test");
-        assertNotNull(update);
-        List<Qualifier<?>> qualifiers = update.getQualifiers();
-        assertEquals(1, qualifiers.size());
-        Qualifier<?> qualifier = qualifiers.get(0);
-        assertEquals(key1, qualifier.getKey());
-        assertEquals(Criteria.EQUALS, qualifier.getCriteria());
-        assertEquals("test", qualifier.getValue());
-        update = update.set(key1, "fluff");
-        assertNotNull(update);
-        List<WebUpdate.UpdateValue> updates = update.getUpdates();
-        assertEquals(1, updates.size());
-        assertEquals("fluff", updates.get(0).getValue());
-        assertEquals(key1, updates.get(0).getKey());
-        assertEquals("java.lang.String", updates.get(0).getValueClass());
-    }
-
-    @Test
-    public void testUpdate() throws UnsupportedEncodingException, IOException, JsonSyntaxException, ClassNotFoundException {
-
-        WebUpdate update = storage.createUpdate().from(category).where(key1, "test").set(key1, "fluff").set(key2, 42);
-        storage.updatePojo(update);
-
-        Gson gson = new Gson();
-        StringReader reader = new StringReader(requestBody);
-        BufferedReader bufRead = new BufferedReader(reader);
-        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
-        String [] params = line.split("&");
-        assertEquals(2, params.length);
-        String[] parts = params[0].split("=");
-        assertEquals("update", parts[0]);
-        WebUpdate receivedUpdate = gson.fromJson(parts[1], WebUpdate.class);
-        assertEquals(42, receivedUpdate.getCategoryId());
-
-        List<WebUpdate.UpdateValue> updates = receivedUpdate.getUpdates();
-        assertEquals(2, updates.size());
-
-        WebUpdate.UpdateValue update1 = updates.get(0);
-        assertEquals(key1, update1.getKey());
-        assertEquals("java.lang.String", update1.getValueClass());
-        assertNull(update1.getValue());
-
-        WebUpdate.UpdateValue update2 = updates.get(1);
-        assertEquals(key2, update2.getKey());
-        assertEquals("java.lang.Integer", update2.getValueClass());
-        assertNull(update2.getValue());
-
-        List<Qualifier<?>> qualifiers = receivedUpdate.getQualifiers();
-        assertEquals(1, qualifiers.size());
-        Qualifier<?> qualifier = qualifiers.get(0);
-        assertEquals(key1, qualifier.getKey());
-        assertEquals(Criteria.EQUALS, qualifier.getCriteria());
-        assertEquals("test", qualifier.getValue());
-
-        parts = params[1].split("=");
-        assertEquals(2, parts.length);
-        assertEquals("values", parts[0]);
-        JsonParser jsonParser = new JsonParser();
-        JsonArray jsonArray = jsonParser.parse(parts[1]).getAsJsonArray();
-        String value1 = gson.fromJson(jsonArray.get(0), String.class);
-        assertEquals("fluff", value1);
-        int value2 = gson.fromJson(jsonArray.get(1), Integer.class);
-        assertEquals(42, value2);
-    }
-
-    @Test
-    public void testGetCount() throws UnsupportedEncodingException, IOException {
-
-        Gson gson = new Gson();
-        responseBody = gson.toJson(12345);
-
-        long result = storage.getCount(category);
-
-        StringReader reader = new StringReader(requestBody);
-        BufferedReader bufRead = new BufferedReader(reader);
-        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
-        String[] parts = line.split("=");
-        assertEquals("category", parts[0]);
-        assertEquals("42", parts[1]);
-        assertEquals(12345, result);
-    }
-
-    @Test
-    public void testSaveFile() {
-        String data = "Hello World";
-        ByteArrayInputStream in = new ByteArrayInputStream(data.getBytes());
-        storage.saveFile("fluff", in);
-        assertEquals("chunked", headers.get("Transfer-Encoding"));
-        String contentType = headers.get("Content-Type");
-        assertTrue(contentType.startsWith("multipart/form-data; boundary="));
-        String boundary = contentType.split("boundary=")[1];
-        String[] lines = requestBody.split("\n");
-        assertEquals("--" + boundary, lines[0].trim());
-        assertEquals("Content-Disposition: form-data; name=\"file\"; filename=\"fluff\"", lines[1].trim());
-        assertEquals("Content-Type: application/octet-stream", lines[2].trim());
-        assertEquals("Content-Transfer-Encoding: binary", lines[3].trim());
-        assertEquals("", lines[4].trim());
-        assertEquals("Hello World", lines[5].trim());
-        assertEquals("--" + boundary + "--", lines[6].trim());
-        
-    }
-
-    @Test
-    public void testLoadFile() throws IOException {
-        responseBody = "Hello World";
-        InputStream in = storage.loadFile("fluff");
-        assertEquals("file=fluff", requestBody.trim());
-        byte[] data = new byte[11];
-        int totalRead = 0;
-        while (totalRead < 11) {
-            int read = in.read(data, totalRead, 11 - totalRead);
-            if (read < 0) {
-                fail();
-            }
-            totalRead += read;
-        }
-        assertEquals("Hello World", new String(data));
-
-    }
-
-    @Test
-    public void testPurge() {
-        storage.purge();
-        assertEquals("POST", method);
-        assertTrue(requestURI.endsWith("/purge"));
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/client/src/test/java/com/redhat/thermostat/web/client/internal/TestObj.java	Thu Dec 06 18:53:24 2012 +0100
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+
+package com.redhat.thermostat.web.client.internal;
+
+import com.redhat.thermostat.storage.core.Entity;
+import com.redhat.thermostat.storage.core.Persist;
+import com.redhat.thermostat.storage.model.BasePojo;
+
+@Entity
+public class TestObj extends BasePojo {
+
+    
+    private String property1;
+
+    @Persist
+    public void setProperty1(String property1) {
+        this.property1 = property1;
+    }
+
+    @Persist
+    public String getProperty1() {
+        return property1;
+    }
+
+    public boolean equals(Object o) {
+        if (! (o instanceof TestObj)) {
+            return false;
+        }
+        TestObj other = (TestObj) o;
+        return property1.equals(other.property1);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java	Thu Dec 06 18:53:24 2012 +0100
@@ -0,0 +1,473 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+
+package com.redhat.thermostat.web.client.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import com.redhat.thermostat.storage.core.Categories;
+import com.redhat.thermostat.storage.core.Category;
+import com.redhat.thermostat.storage.core.Cursor;
+import com.redhat.thermostat.storage.core.Key;
+import com.redhat.thermostat.storage.core.Query;
+import com.redhat.thermostat.storage.core.Remove;
+import com.redhat.thermostat.storage.core.Query.Criteria;
+import com.redhat.thermostat.test.FreePortFinder;
+import com.redhat.thermostat.test.FreePortFinder.TryPort;
+import com.redhat.thermostat.web.client.internal.WebStorage;
+import com.redhat.thermostat.web.common.Qualifier;
+import com.redhat.thermostat.web.common.WebQuery;
+import com.redhat.thermostat.web.common.WebInsert;
+import com.redhat.thermostat.web.common.WebRemove;
+import com.redhat.thermostat.web.common.WebUpdate;
+
+public class WebStorageTest {
+
+    private Server server;
+
+    private int port;
+
+    private String requestBody;
+
+    private String responseBody;
+    private Map<String,String> headers;
+    private String method;
+    private String requestURI;
+
+    private static Category category;
+    private static Key<String> key1;
+    private static Key<Integer> key2;
+
+    private WebStorage storage;
+
+    @BeforeClass
+    public static void setupCategory() {
+        key1 = new Key<>("property1", true);
+        key2 = new Key<>("property2", true);
+        category = new Category("test", key1);
+    }
+
+    @AfterClass
+    public static void cleanupCategory() {
+        Categories.remove(category);
+        category = null;
+        key1 = null;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        
+        port = FreePortFinder.findFreePort(new TryPort() {
+            @Override
+            public void tryPort(int port) throws Exception {
+                startServer(port);
+            }
+        });
+
+        storage = new WebStorage();
+        storage.setEndpoint("http://localhost:" + port + "/");
+        storage.setAgentId(new UUID(123, 456));
+        headers = new HashMap<>();
+        requestURI = null;
+        method = null;
+        registerCategory();
+    }
+
+    private void startServer(int port) throws Exception {
+        server = new Server(port);
+        server.setHandler(new AbstractHandler() {
+            
+            @Override
+            public void handle(String target, Request baseRequest,
+                    HttpServletRequest request, HttpServletResponse response)
+                    throws IOException, ServletException {
+                Enumeration<String> headerNames = request.getHeaderNames();
+                while (headerNames.hasMoreElements()) {
+                    String headerName = headerNames.nextElement();
+                    headers.put(headerName, request.getHeader(headerName));
+                }
+
+                method = request.getMethod();
+                requestURI = request.getRequestURI();
+
+                // Read request body.
+                StringBuilder body = new StringBuilder();
+                Reader reader = request.getReader();
+                while (true) {
+                    int read = reader.read();
+                    if (read == -1) {
+                        break;
+                    }
+                    body.append((char) read);
+                }
+                requestBody = body.toString();
+                // Send response body.
+                response.setStatus(HttpServletResponse.SC_OK);
+                if (responseBody != null) {
+                    response.getWriter().write(responseBody);
+                }
+                baseRequest.setHandled(true);
+            }
+        });
+        server.start();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+
+        headers = null;
+        requestURI = null;
+        method = null;
+        storage = null;
+
+        server.stop();
+        server.join();
+    }
+
+    private void registerCategory() {
+
+        // Return 42 for categoryId.
+        Gson gson = new Gson();
+        responseBody = gson.toJson(42);
+
+        storage.registerCategory(category);
+    }
+
+    @Test
+    public void testFindPojo() throws UnsupportedEncodingException, IOException {
+
+        TestObj obj = new TestObj();
+        obj.setProperty1("fluffor");
+        Gson gson = new Gson();
+        responseBody = gson.toJson(obj);
+
+        Query query = storage.createQuery().from(category).where(key1, Criteria.EQUALS, "fluff");
+
+        TestObj result = storage.findPojo(query, TestObj.class);
+        StringReader reader = new StringReader(requestBody);
+        BufferedReader bufRead = new BufferedReader(reader);
+        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
+        String[] parts = line.split("=");
+        assertEquals("query", parts[0]);
+        WebQuery restQuery = gson.fromJson(parts[1], WebQuery.class);
+
+        assertEquals(42, restQuery.getCategoryId());
+        List<Qualifier<?>> qualifiers = restQuery.getQualifiers();
+        assertEquals(1, qualifiers.size());
+        Qualifier<?> qual = qualifiers.get(0);
+        assertEquals(new Key<String>("property1", true), qual.getKey());
+        assertEquals(Criteria.EQUALS, qual.getCriteria());
+        assertEquals("fluff", qual.getValue());
+
+        assertEquals("fluffor", result.getProperty1());
+    }
+
+    @Test
+    public void testFindAllPojos() throws UnsupportedEncodingException, IOException {
+
+        TestObj obj1 = new TestObj();
+        obj1.setProperty1("fluffor1");
+        TestObj obj2 = new TestObj();
+        obj2.setProperty1("fluffor2");
+        Gson gson = new Gson();
+        responseBody = gson.toJson(Arrays.asList(obj1, obj2));
+
+        Key<String> key1 = new Key<>("property1", true);
+        Query query = storage.createQuery().from(category).where(key1, Criteria.EQUALS, "fluff");
+
+        Cursor<TestObj> results = storage.findAllPojos(query, TestObj.class);
+        StringReader reader = new StringReader(requestBody);
+        BufferedReader bufRead = new BufferedReader(reader);
+        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
+        String[] parts = line.split("=");
+        assertEquals("query", parts[0]);
+        WebQuery restQuery = gson.fromJson(parts[1], WebQuery.class);
+
+        assertEquals(42, restQuery.getCategoryId());
+        List<Qualifier<?>> qualifiers = restQuery.getQualifiers();
+        assertEquals(1, qualifiers.size());
+        Qualifier<?> qual = qualifiers.get(0);
+        assertEquals(new Key<String>("property1", true), qual.getKey());
+        assertEquals(Criteria.EQUALS, qual.getCriteria());
+        assertEquals("fluff", qual.getValue());
+
+        assertTrue(results.hasNext());
+        assertEquals("fluffor1", results.next().getProperty1());
+        assertTrue(results.hasNext());
+        assertEquals("fluffor2", results.next().getProperty1());
+        assertFalse(results.hasNext());
+    }
+
+    @Test
+    public void testPut() throws IOException, JsonSyntaxException, ClassNotFoundException {
+
+        TestObj obj = new TestObj();
+        obj.setProperty1("fluff");
+
+        storage.putPojo(category, true, obj);
+
+        Gson gson = new Gson();
+        StringReader reader = new StringReader(requestBody);
+        BufferedReader bufRead = new BufferedReader(reader);
+        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
+        String [] params = line.split("&");
+        assertEquals(2, params.length);
+        String[] parts = params[0].split("=");
+        assertEquals("insert", parts[0]);
+        WebInsert insert = gson.fromJson(parts[1], WebInsert.class);
+        assertEquals(42, insert.getCategoryId());
+        assertEquals(true, insert.isReplace());
+        assertEquals(TestObj.class.getName(), insert.getPojoClass());
+
+        parts = params[1].split("=");
+        assertEquals(2, parts.length);
+        assertEquals("pojo", parts[0]);
+        Object resultObj = gson.fromJson(parts[1], Class.forName(insert.getPojoClass()));
+        assertEquals(obj, resultObj);
+    }
+
+    @Test
+    public void testCreateRemove() {
+        WebRemove remove = (WebRemove) storage.createRemove();
+        assertNotNull(remove);
+        remove = remove.from(category);
+        assertEquals(42, remove.getCategoryId());
+        assertNotNull(remove);
+        remove = remove.where(key1, "test");
+        assertNotNull(remove);
+        List<Qualifier<?>> qualifiers = remove.getQualifiers();
+        assertEquals(1, qualifiers.size());
+        Qualifier<?> qualifier = qualifiers.get(0);
+        assertEquals(key1, qualifier.getKey());
+        assertEquals(Criteria.EQUALS, qualifier.getCriteria());
+        assertEquals("test", qualifier.getValue());
+    }
+
+    @Test
+    public void testRemovePojo() throws UnsupportedEncodingException, IOException {
+        Remove remove = storage.createRemove().from(category).where(key1, "test");
+        storage.removePojo(remove);
+
+        Gson gson = new Gson();
+        StringReader reader = new StringReader(requestBody);
+        BufferedReader bufRead = new BufferedReader(reader);
+        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
+        System.err.println("line: " + line);
+        String[] parts = line.split("=");
+        assertEquals("remove", parts[0]);
+        WebRemove actualRemove = gson.fromJson(parts[1], WebRemove.class);
+        
+        assertEquals(42, actualRemove.getCategoryId());
+        List<Qualifier<?>> qualifiers = actualRemove.getQualifiers();
+        assertEquals(1, qualifiers.size());
+        Qualifier<?> qualifier = qualifiers.get(0);
+        assertEquals(key1, qualifier.getKey());
+        assertEquals(Criteria.EQUALS, qualifier.getCriteria());
+        assertEquals("test", qualifier.getValue());
+    }
+
+    @Test
+    public void testCreateUpdate() {
+        WebUpdate update = (WebUpdate) storage.createUpdate();
+        assertNotNull(update);
+        update = update.from(category);
+        assertEquals(42, update.getCategoryId());
+        assertNotNull(update);
+        update = update.where(key1, "test");
+        assertNotNull(update);
+        List<Qualifier<?>> qualifiers = update.getQualifiers();
+        assertEquals(1, qualifiers.size());
+        Qualifier<?> qualifier = qualifiers.get(0);
+        assertEquals(key1, qualifier.getKey());
+        assertEquals(Criteria.EQUALS, qualifier.getCriteria());
+        assertEquals("test", qualifier.getValue());
+        update = update.set(key1, "fluff");
+        assertNotNull(update);
+        List<WebUpdate.UpdateValue> updates = update.getUpdates();
+        assertEquals(1, updates.size());
+        assertEquals("fluff", updates.get(0).getValue());
+        assertEquals(key1, updates.get(0).getKey());
+        assertEquals("java.lang.String", updates.get(0).getValueClass());
+    }
+
+    @Test
+    public void testUpdate() throws UnsupportedEncodingException, IOException, JsonSyntaxException, ClassNotFoundException {
+
+        WebUpdate update = storage.createUpdate().from(category).where(key1, "test").set(key1, "fluff").set(key2, 42);
+        storage.updatePojo(update);
+
+        Gson gson = new Gson();
+        StringReader reader = new StringReader(requestBody);
+        BufferedReader bufRead = new BufferedReader(reader);
+        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
+        String [] params = line.split("&");
+        assertEquals(2, params.length);
+        String[] parts = params[0].split("=");
+        assertEquals("update", parts[0]);
+        WebUpdate receivedUpdate = gson.fromJson(parts[1], WebUpdate.class);
+        assertEquals(42, receivedUpdate.getCategoryId());
+
+        List<WebUpdate.UpdateValue> updates = receivedUpdate.getUpdates();
+        assertEquals(2, updates.size());
+
+        WebUpdate.UpdateValue update1 = updates.get(0);
+        assertEquals(key1, update1.getKey());
+        assertEquals("java.lang.String", update1.getValueClass());
+        assertNull(update1.getValue());
+
+        WebUpdate.UpdateValue update2 = updates.get(1);
+        assertEquals(key2, update2.getKey());
+        assertEquals("java.lang.Integer", update2.getValueClass());
+        assertNull(update2.getValue());
+
+        List<Qualifier<?>> qualifiers = receivedUpdate.getQualifiers();
+        assertEquals(1, qualifiers.size());
+        Qualifier<?> qualifier = qualifiers.get(0);
+        assertEquals(key1, qualifier.getKey());
+        assertEquals(Criteria.EQUALS, qualifier.getCriteria());
+        assertEquals("test", qualifier.getValue());
+
+        parts = params[1].split("=");
+        assertEquals(2, parts.length);
+        assertEquals("values", parts[0]);
+        JsonParser jsonParser = new JsonParser();
+        JsonArray jsonArray = jsonParser.parse(parts[1]).getAsJsonArray();
+        String value1 = gson.fromJson(jsonArray.get(0), String.class);
+        assertEquals("fluff", value1);
+        int value2 = gson.fromJson(jsonArray.get(1), Integer.class);
+        assertEquals(42, value2);
+    }
+
+    @Test
+    public void testGetCount() throws UnsupportedEncodingException, IOException {
+
+        Gson gson = new Gson();
+        responseBody = gson.toJson(12345);
+
+        long result = storage.getCount(category);
+
+        StringReader reader = new StringReader(requestBody);
+        BufferedReader bufRead = new BufferedReader(reader);
+        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
+        String[] parts = line.split("=");
+        assertEquals("category", parts[0]);
+        assertEquals("42", parts[1]);
+        assertEquals(12345, result);
+    }
+
+    @Test
+    public void testSaveFile() {
+        String data = "Hello World";
+        ByteArrayInputStream in = new ByteArrayInputStream(data.getBytes());
+        storage.saveFile("fluff", in);
+        assertEquals("chunked", headers.get("Transfer-Encoding"));
+        String contentType = headers.get("Content-Type");
+        assertTrue(contentType.startsWith("multipart/form-data; boundary="));
+        String boundary = contentType.split("boundary=")[1];
+        String[] lines = requestBody.split("\n");
+        assertEquals("--" + boundary, lines[0].trim());
+        assertEquals("Content-Disposition: form-data; name=\"file\"; filename=\"fluff\"", lines[1].trim());
+        assertEquals("Content-Type: application/octet-stream", lines[2].trim());
+        assertEquals("Content-Transfer-Encoding: binary", lines[3].trim());
+        assertEquals("", lines[4].trim());
+        assertEquals("Hello World", lines[5].trim());
+        assertEquals("--" + boundary + "--", lines[6].trim());
+        
+    }
+
+    @Test
+    public void testLoadFile() throws IOException {
+        responseBody = "Hello World";
+        InputStream in = storage.loadFile("fluff");
+        assertEquals("file=fluff", requestBody.trim());
+        byte[] data = new byte[11];
+        int totalRead = 0;
+        while (totalRead < 11) {
+            int read = in.read(data, totalRead, 11 - totalRead);
+            if (read < 0) {
+                fail();
+            }
+            totalRead += read;
+        }
+        assertEquals("Hello World", new String(data));
+
+    }
+
+    @Test
+    public void testPurge() {
+        storage.purge();
+        assertEquals("POST", method);
+        assertTrue(requestURI.endsWith("/purge"));
+    }
+}