You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by dk...@apache.org on 2021/11/05 18:17:39 UTC

[cxf-site] 01/01: Copy cxf site builder over

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

dkulp pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cxf-site.git

commit 77215b884b0b2d880ffb862dd26a9abdf9648e6a
Author: Daniel Kulp <dk...@apache.org>
AuthorDate: Fri Nov 5 14:16:02 2021 -0400

    Copy cxf site builder over
---
 .asf.yaml                                          |   13 +
 .gitignore                                         |    1 +
 README                                             |   40 +
 content/.dir-save                                  |    0
 pom.xml                                            |  169 +++
 .../java/org/apache/cxf/cwiki/AbstractPage.java    |  123 ++
 .../org/apache/cxf/cwiki/BlogEntrySummary.java     |   83 ++
 .../apache/cxf/cwiki/ConfluenceCleanupWriter.java  |  444 +++++++
 src/main/java/org/apache/cxf/cwiki/Page.java       |  655 ++++++++++
 .../java/org/apache/cxf/cwiki/PageManager.java     |   63 +
 src/main/java/org/apache/cxf/cwiki/Renderer.java   |   52 +
 .../java/org/apache/cxf/cwiki/SiteExporter.java    | 1313 ++++++++++++++++++++
 src/main/java/org/apache/cxf/cwiki/Space.java      |   53 +
 src/main/resources/confluence-rest.wadl            |  451 +++++++
 src/test/java/org/apache/cxf/cwiki/PageTest.java   |   55 +
 src/test/resources/docs.cfg                        |    2 +
 src/test/resources/page.xml                        |   29 +
 template/docs.cfg                                  |    6 +
 template/main.cfg                                  |    6 +
 template/template.vm                               |  162 +++
 update-site                                        |    4 +
 21 files changed, 3724 insertions(+)

diff --git a/.asf.yaml b/.asf.yaml
new file mode 100644
index 0000000..c3d504d
--- /dev/null
+++ b/.asf.yaml
@@ -0,0 +1,13 @@
+github:
+  description: "Apache CXF website"
+  homepage: https://cxf.apache.org/
+
+staging:
+  profile: ~
+  whoami:  asf-staging
+
+
+notifications:
+  commits: commits@cxf.apache.org
+  issues: issues@cxf.apache.org
+  pullrequests: dev@cxf.apache.org
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eb5a316
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+target
diff --git a/README b/README
new file mode 100644
index 0000000..355fc01
--- /dev/null
+++ b/README
@@ -0,0 +1,40 @@
+
+This is the start of an export utility to export the CXF
+confluence spaces to the sites.    To run this manually, you need to 
+have a copy of the CXF website checked out to a local directory:
+
+svn checkout https://svn.apache.org/repos/infra/websites/production/cxf/content
+
+Build:
+mvn install
+
+You will need to pass credentials for a valid confluence user.  This can be
+done in one of two ways:
+
+1) Command line:
+   -Dconfluence.user=username -Dconfluence.password=password
+
+2) Profile in .m2/settings.xml
+       <profile>
+            <id>confluence</id>
+            <properties>
+                <confluence.user>user</confluence.user>
+                <confluence.password>passwd</confluence.password>
+            </properties>
+        </profile>
+
+To export the space:
+mvn -Pconfluence exec:java -Dcxf.site.output=/path/to/content
+should appear in /path/to/content/
+
+In the confluence profile above, you could add a property like:
+   <cxf.site.output>/path/to/content</cxf.site.output>
+to simplify passing that into the command.
+
+When you commit any changes in the content directory, the changes 
+should be "live" fairly quickly due to the svnpubsub process.
+
+
+
+
+
diff --git a/content/.dir-save b/content/.dir-save
new file mode 100644
index 0000000..e69de29
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..9554696
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,169 @@
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements. See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership. The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License. You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied. See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.apache.cxf.site-export</groupId>
+    <artifactId>cxf-site-export</artifactId>
+    <packaging>jar</packaging>
+    <version>1.0-SNAPSHOT</version>
+    <name>Apache CXF Confluence Site Exporter</name>
+    <url>http://cxf.apache.org</url>
+
+    <parent>
+        <groupId>org.apache.cxf</groupId>
+        <artifactId>cxf-parent</artifactId>
+        <version>3.1.15</version>
+    </parent>
+
+    <properties>
+        <enforcer.skip>true</enforcer.skip>
+        <cxf.version>3.1.15</cxf.version>
+        <extra.arg></extra.arg>
+        <svn.arg1></svn.arg1>
+        <svn.arg2></svn.arg2>
+        <site.output>${basedir}/content</site.output>
+        <cxf.site.output>${site.output}</cxf.site.output>
+        <!--maven.test.skip>true</maven.test.skip-->
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-frontend-jaxws</artifactId>
+            <version>${cxf.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-frontend-jaxrs</artifactId>
+            <version>${cxf.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-rs-client</artifactId>
+            <version>${cxf.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-transports-http</artifactId>
+            <version>${cxf.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-features-clustering</artifactId>
+            <version>${cxf.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.velocity</groupId>
+            <artifactId>velocity</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.ccil.cowan.tagsoup</groupId>
+            <artifactId>tagsoup</artifactId>
+            <version>1.2.1</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <version>2.5.4</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.testng</groupId>
+            <artifactId>testng</artifactId>
+            <version>6.8</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.codehaus.mojo</groupId>
+                    <artifactId>exec-maven-plugin</artifactId>
+                    <version>1.2</version>
+                    <configuration>
+                        <mainClass>org.apache.cxf.cwiki.SiteExporter</mainClass>
+                        <arguments>
+                            <argument>-d</argument>
+                            <argument>${cxf.site.output}</argument>
+                            <argument>-password</argument>
+                            <argument>${confluence.password}</argument>
+                            <argument>-user</argument>
+                            <argument>${confluence.user}</argument>
+                            <argument>${extra.arg}</argument>
+                            <argument>${svn.arg1}</argument>
+                            <argument>${svn.arg2}</argument>
+                            <!--argument>-force</argument-->
+                            <argument>${basedir}/template/main.cfg</argument>
+                            <argument>${basedir}/template/docs.cfg</argument>
+                        </arguments>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.cxf</groupId>
+                <artifactId>cxf-wadl2java-plugin</artifactId>
+                <version>${cxf.version}</version>
+                <executions>
+                    <execution>
+                        <id>generate-sources</id>
+                        <phase>generate-sources</phase>
+                        <configuration>
+                            <sourceRoot>${basedir}/target/generated/src/main/java</sourceRoot>
+                            <wadlOptions>
+                                <wadlOption>
+                                    <wadl>${basedir}/src/main/resources/confluence-rest.wadl</wadl>
+                                </wadlOption>
+                            </wadlOptions>
+                        </configuration>
+                        <goals>
+                            <goal>wadl2java</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+
+    <profiles>
+        <profile>
+            <id>svn</id>
+            <properties>
+                <svn.arg1>-svn</svn.arg1>
+                <svn.arg2>-commit</svn.arg2>
+            </properties>
+        </profile>
+        <profile>
+            <id>forceAll</id>
+            <properties>
+                <svn.arg1>-force</svn.arg1>
+            </properties>
+        </profile>
+    </profiles>
+</project>
diff --git a/src/main/java/org/apache/cxf/cwiki/AbstractPage.java b/src/main/java/org/apache/cxf/cwiki/AbstractPage.java
new file mode 100644
index 0000000..37bc31c
--- /dev/null
+++ b/src/main/java/org/apache/cxf/cwiki/AbstractPage.java
@@ -0,0 +1,123 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.cwiki;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.w3c.dom.Element;
+
+import org.apache.cxf.helpers.DOMUtils;
+
+/**
+ * 
+ */
+public class AbstractPage implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+    
+    final String id;
+    final String title;
+    final String url;
+
+    Map<String, String> attachments;
+    
+    transient String directory;
+    
+    public AbstractPage(Element root) throws Exception {
+        // org.apache.cxf.helpers.XMLUtils.printDOM(doc.getDocumentElement());
+
+        id = DOMUtils.getChildContent(root, "id");
+        title = DOMUtils.getChildContent(root, "title");
+        url = DOMUtils.getChildContent(root, "url");
+    }
+    
+    public AbstractPage(AbstractPage source) {
+        this.id = source.id;
+        this.title = source.title;
+        this.url = source.url;
+        this.directory = source.directory;
+    }
+    
+    public String getDirectory() {
+        return directory == null ? "" : directory;
+    }
+    
+    public String getPath() {
+        return getDirectory() + createFileName();
+    }
+    
+    public String createFileName() {
+        StringBuffer buffer = new StringBuffer();
+        char array[] = title.toLowerCase().toCharArray();
+        boolean separated = true;
+        for (int x = 0; x < array.length; x++) {
+            if ("abcdefghijklmnopqrstuvwxyz0123456789".indexOf(array[x]) >= 0) {
+                buffer.append(Character.toLowerCase(array[x]));
+                separated = false;
+            } else if ("\r\n\t -".indexOf(array[x]) >= 0) {
+                if (separated) {
+                    continue;
+                }
+                buffer.append('-');
+                separated = true;
+            }
+        }
+        if (buffer.length() == 0) {
+            return id + ".html";
+        }
+        return buffer.append(".html").toString();
+    }
+    
+    public String getId() {
+        return id;
+    }
+    
+    public String getTitle() {
+        return title;
+    }
+    
+    public String getURL() {
+        return url;
+    }
+    
+    public boolean getHasCode() {
+        return false;
+    }
+
+    public String toString() {
+        return "AbstractPage[id=" + id + ",title=" + title + ",url=" + url + "]";
+    }
+    
+    public void addAttachment(String aid, String filename) {
+        if (attachments == null) {
+            attachments = new HashMap<String, String>();
+        }
+        attachments.put(aid, filename);
+    }
+    public String getAttachmentFilename(String aid) {
+        if (attachments == null) {
+            return null;
+        }
+        return attachments.get(aid);
+    }
+    
+}
diff --git a/src/main/java/org/apache/cxf/cwiki/BlogEntrySummary.java b/src/main/java/org/apache/cxf/cwiki/BlogEntrySummary.java
new file mode 100644
index 0000000..1434e35
--- /dev/null
+++ b/src/main/java/org/apache/cxf/cwiki/BlogEntrySummary.java
@@ -0,0 +1,83 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.cwiki;
+
+import java.io.Serializable;
+
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.datatype.XMLGregorianCalendar;
+import org.w3c.dom.Element;
+
+import org.apache.cxf.helpers.DOMUtils;
+
+/**
+ * 
+ */
+public class BlogEntrySummary extends AbstractPage implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+    
+    final XMLGregorianCalendar published;
+    
+    // BlogEntrySummary does not have version field but the BlogEntry does.
+    // We load and set the version separately. It will be used to decide 
+    // whether the blog entry needs to be re-rendered or not.
+    int version;
+    
+    public BlogEntrySummary(Element root) throws Exception {
+        super(root);
+
+        String mod = DOMUtils.getChildContent(root, "publishDate");
+        published = DatatypeFactory.newInstance().newXMLGregorianCalendar(mod);
+    }
+    
+    public String getDirectory() {
+        StringBuilder builder = new StringBuilder();
+        builder.append(String.valueOf(published.getYear()));
+        builder.append("/");
+        if (published.getMonth() < 10) {
+            builder.append("0");
+        } 
+        builder.append(String.valueOf(published.getMonth()));
+        builder.append("/");
+        if (published.getDay() < 10) {
+            builder.append("0");
+        } 
+        builder.append(String.valueOf(published.getDay()));
+        builder.append("/");
+        return builder.toString();
+    }
+    
+    public int getVersion() {
+        return version;
+    }
+    
+    void setVersion(int version) {
+        this.version = version;
+    }
+    
+    public XMLGregorianCalendar getPublished() {
+        return published;
+    }
+    
+    public String toString() {
+        return "BlogEntrySummary[id=" + id + ",title=" + title + ",version=" + version + ",url=" + url + "]";
+    }
+}
diff --git a/src/main/java/org/apache/cxf/cwiki/ConfluenceCleanupWriter.java b/src/main/java/org/apache/cxf/cwiki/ConfluenceCleanupWriter.java
new file mode 100644
index 0000000..c828545
--- /dev/null
+++ b/src/main/java/org/apache/cxf/cwiki/ConfluenceCleanupWriter.java
@@ -0,0 +1,444 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.cwiki;
+
+import java.io.File;
+import java.io.Writer;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Stack;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+import org.ccil.cowan.tagsoup.XMLWriter;
+
+/**
+ * 
+ */
+public class ConfluenceCleanupWriter extends XMLWriter {
+
+    private final AbstractPage page;
+    private final SiteExporter exporter;
+    private final String divId;
+    private final String divCls;
+    private final Stack<Integer> trStack = new Stack<Integer>(); 
+    private int curTrCount;
+
+    public ConfluenceCleanupWriter(SiteExporter exp, Writer writer, AbstractPage page, 
+                                   String id, String divCls) {
+        super(writer);
+        this.page = page;
+        this.exporter = exp;
+        this.divId = id;
+        this.divCls = divCls;
+    }
+
+    private File getPageDirectory() {
+        String pageDir = page.getDirectory();
+        if (pageDir.length() > 0) {
+            return new File(exporter.outputDir, pageDir);
+        } else {
+            return exporter.outputDir;
+        }
+    }
+    
+    private String findPageWithURL(String url) throws Exception {
+        String location = findPageWithURL(exporter, url);
+        if (location == null) {
+            for (SiteExporter siteExporter : SiteExporter.siteExporters) {
+                if (exporter == siteExporter) {
+                    continue;
+                }
+                location = findPageWithURL(siteExporter, url);
+                if (location != null) {
+                    break;
+                }
+            }
+        }
+        return location;
+    }
+    
+    private String findPageWithURL(SiteExporter siteExporter, String url) throws Exception {
+        if (siteExporter.getSpace().getURL().endsWith(url)) {
+            String prefix = getRelativePath(SiteExporter.rootOutputDir, getPageDirectory(), siteExporter.outputDir);
+            String location = prefix + "index.html";
+            if (exporter != siteExporter) {
+                System.out.println("Cross space link to " + location);
+            }
+            return location;
+        } else {
+            AbstractPage p = siteExporter.findPageByURL(url);
+            if (p == null) {
+                p = siteExporter.findBlogEntryByURL(url);
+            }
+            if (p != null) {
+                String prefix = getRelativePath(SiteExporter.rootOutputDir, getPageDirectory(), siteExporter.outputDir);
+                String location = prefix + p.getPath();
+                if (exporter != siteExporter) {
+                    System.out.println("Cross space link to " + location);
+                }
+                return location;
+            }
+        }
+        return null;
+    }
+    
+    private String findPageByID(String id) throws Exception {
+        String location = findPageByID(exporter, id);        
+        if (location == null) {
+            for (SiteExporter siteExporter : SiteExporter.siteExporters) {
+                if (exporter == siteExporter) {
+                    continue;
+                }
+                location = findPageByID(siteExporter, id);
+                if (location != null) {
+                    break;
+                }
+            }
+        }
+        return location;
+    }
+    
+    private String findPageByID(SiteExporter siteExporter, String url) throws Exception {
+        AbstractPage p = siteExporter.findPageByID(url);
+        if (p != null) {
+            String prefix = getRelativePath(SiteExporter.rootOutputDir, getPageDirectory(), siteExporter.outputDir);
+            String location = prefix + p.getPath();
+            if (exporter != siteExporter) {
+                System.out.println("Cross space link (via id) to " + location);
+            }
+            return location;
+        }
+        return null;
+    }
+    
+    //CHECKSTYLE:OFF
+    public void startElement(String uri, String localName, String qName, final Attributes atts)
+        throws SAXException {
+        AttributesWrapper newAtts = new AttributesWrapper(atts);
+        if ("a".equals(localName.toLowerCase())
+            || "a".equals(qName.toLowerCase())) {
+            String href = atts.getValue("href");
+            //Confluence sticks this on links from blog entries, but it's invalid
+            newAtts.remove("data-username");
+            if (href != null) {
+                href = href.trim();
+            }
+            if (href != null && href.startsWith("/confluence/display/")) {
+                String params = "";
+                if (href.indexOf('#') != -1) {
+                    params = href.substring(href.indexOf('#'));
+                    href = href.substring(0, href.indexOf('#'));
+                }
+                if (href.indexOf('?') != -1) {
+                    if (params.length() > 0) {
+                        params = href.substring(href.indexOf('?')) + "#" + params;
+                    } else {
+                        params = href.substring(href.indexOf('?'));
+                    }
+                    href = href.substring(0, href.indexOf('?'));
+                }
+                try {
+                    String p = findPageWithURL(href);
+                    if (p != null) {
+                        newAtts.addMapping("href", p + params);
+                    } else {
+                        if (href.indexOf('~') == -1) {
+                            //link to a user page is OK, don't warn about it
+                            System.out.println("Could not find page for " + href 
+                                               + " linked from " + page.getTitle());
+                        }
+                        newAtts.addMapping("href", SiteExporter.ROOT + href.substring(11));
+                    }
+                } catch (Exception e) {
+                    throw new SAXException(e);
+                }
+            } else if (href != null && href.startsWith("/confluence/plugins/")) {
+                newAtts.addMapping("href", SiteExporter.ROOT + href.substring(11));
+            } else if (href != null && href.contains("/confluence/pages/viewpage.action")) {
+                String params = "";
+                if (href.indexOf('#') != -1) {
+                    params = href.substring(href.indexOf('#'));
+                    href = href.substring(0, href.indexOf('#'));
+                }
+                int idx = href.indexOf("pageId=");
+                String id = href.substring(idx + 7);
+                try {
+                    String location = findPageByID(id);
+                    if (location != null) {
+                          newAtts.addMapping("href", location + params);
+                    } else {
+                        System.out.println("Could not find page for id: " + id 
+                                           + " linked from " + page.getTitle());
+                    }   
+                } catch (Exception e) {
+                    throw new SAXException(e);
+                }
+            } else if (href != null && href.contains("/confluence/download/attachments")) {
+                href = href.substring(href.lastIndexOf("/"));
+                String dirName = page.createFileName();
+                dirName = dirName.substring(0, dirName.lastIndexOf(".")) + ".data";
+
+                newAtts.addMapping("href", dirName + href);
+            } else if (href != null && href.contains("/confluence/pages/createpage.action")) {
+                System.out.println("Adding createpage link for " + href + " from " + page.getTitle());
+                newAtts.addMapping("href", SiteExporter.HOST + href);
+            } else if (href != null 
+                && (href.startsWith("http://")
+                    || href.startsWith("https://"))) {
+                URL url;
+                try {
+                    url = new URL(href);
+                    if (url.getHost().contains("apache.org")) {
+                        newAtts.remove("rel");
+                    }
+                    if (url.getHost().equals("cxf.apache.org")
+                        && "external-link".equals(newAtts.getValue("class"))) {
+                        newAtts.remove("class");
+                    }
+                } catch (MalformedURLException e) {
+                    //ignore
+                }
+            }
+        } else if ("img".equals(localName.toLowerCase())
+            || "img".equals(qName.toLowerCase())) {
+            String href = exporter.stripHost(atts.getValue("src"));
+            if ("absmiddle".equalsIgnoreCase(atts.getValue("align"))) {
+                newAtts.addMapping("align", "middle");
+            }
+            String cls = atts.getValue("class");
+            if (href != null && href.startsWith("/confluence/images/")) {
+                newAtts.addMapping("src", href.replaceFirst("^/confluence/images/", "/images/confluence/"));
+            } else if (href != null && href.startsWith("/confluence/download/attachments")) {
+                if (cls == null || cls.contains("confluence-embedded-image")) {
+                    href = href.substring(0, href.lastIndexOf('?'));
+                    href = href.substring(href.lastIndexOf('/'));
+                    String dirName = page.createFileName();
+                    dirName = dirName.substring(0, dirName.lastIndexOf(".")) + ".data";
+
+                    newAtts.addMapping("src", dirName + href.replaceAll("\\+", "-"));
+                } else if (cls.contains("userLogo")) {
+                    String name = href;
+                    try {
+                        name = exporter.loadUserImage(page, href);
+                    } catch (Exception ex) {
+                        System.out.println("Could not download userLogo " + href 
+                                           + " linked from " + page.getTitle());                    
+                    }
+                    String dirName = page.createFileName();
+                    dirName = dirName.substring(0, dirName.lastIndexOf(".")) + ".userimage/";
+
+                    newAtts.addMapping("src", dirName + name);                    
+                } else {
+                    newAtts.addMapping("src", SiteExporter.HOST + href.replaceAll("\\+", "-"));
+                }
+            } else if (href != null && href.startsWith("/confluence/download/thumbnails")) {
+                String name = href;
+                try {
+                    name = exporter.loadThumbnail(page, href);
+                } catch (Exception ex) {
+                    System.out.println("Could not download thumbnail " + href 
+                                       + " linked from " + page.getTitle());                    
+                }
+                String dirName = page.createFileName();
+                dirName = dirName.substring(0, dirName.lastIndexOf(".")) + ".thumbs/";
+
+                newAtts.addMapping("src", dirName + name);
+            } else if (href != null && href.startsWith("/confluence")) {
+                newAtts.addMapping("src", SiteExporter.HOST + href);
+            }
+        } else if ("th".equals(localName.toLowerCase())
+            || "th".equals(qName.toLowerCase())) {
+            curTrCount++;
+        } else if ("td".equals(localName.toLowerCase())
+            || "td".equals(qName.toLowerCase())) {
+            curTrCount++;
+            if (newAtts.getIndex("nowrap") != -1) {
+                //make sure nowrap attribute is set to nowrap per HTML spec
+                newAtts.addMapping("nowrap", "nowrap");
+            }
+        } else if ("tr".equals(localName.toLowerCase())
+            || "tr".equals(qName.toLowerCase())) {
+            trStack.push(curTrCount);
+            curTrCount = 0;
+        } else if ("div".equals(localName.toLowerCase())
+            || "div".equals(qName.toLowerCase())) {
+            String id = atts.getValue("id");
+            if ("ConfluenceContent".equals(id)) {
+                if (divCls != null) {
+                    newAtts.addMapping("class", divCls);
+                    newAtts.remove("id");
+                }
+                if (divId != null) {
+                    newAtts.addMapping("id", divId);
+                } 
+            }
+        } else if ("input".equals(localName.toLowerCase())
+            || "input".equals(qName.toLowerCase())) {
+            String value = atts.getValue("value");
+            if (value != null && value.startsWith("/confluence/")) {
+                newAtts.addMapping("value", SiteExporter.ROOT + value.substring(11));
+            }
+        } else if ("pre".equals(localName.toLowerCase())
+                   || "pre".equals(qName.toLowerCase())) {
+            String cls = atts.getValue("class");
+            if ("syntaxhighlighter-pre".equalsIgnoreCase(cls)) {
+                String brush = atts.getValue("data-syntaxhighlighter-params");
+                if (brush.toLowerCase().startsWith("brush")) {
+                    newAtts.remove("data-syntaxhighlighter-params");
+                    newAtts.remove("data-theme");
+                    newAtts.addMapping("class", brush);
+                }
+            }
+        }
+        super.startElement(uri, localName, qName, newAtts);
+    }
+    
+    
+    
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+        if ("tr".equals(localName.toLowerCase())
+            || "tr".equals(qName.toLowerCase())) {
+            if (curTrCount == 0) {
+                super.startElement("td");
+                super.endElement("td");
+            }
+            curTrCount = trStack.pop();
+        }
+        super.endElement(uri, localName, qName);
+    }
+
+    final class AttributesWrapper implements Attributes {
+        private final Map<String, String> atts = new LinkedHashMap<String, String>();
+
+        private AttributesWrapper(Attributes atts) {
+            for (int x = 0; x < atts.getLength(); x++) {
+                this.atts.put(atts.getQName(x), atts.getValue(x));
+            }
+        }
+        private Map.Entry<String, String> getByIndex(int i) {
+            for (Map.Entry<String, String> a : atts.entrySet()) {
+                if  (i == 0) {
+                    return a;
+                }
+                --i;
+            }
+            return null;
+        }
+        private int findIndex(String k) {
+            int i = 0;
+            for (Map.Entry<String, String> a : atts.entrySet()) {
+                if  (a.getKey().equals(k)) {
+                    return i;
+                }
+                ++i;
+            }
+            return -1;
+        }
+        
+        public void remove(String k) {
+            atts.remove(k);
+        }
+        
+        public void addMapping(String k, String v) {
+            atts.put(k, v);
+        }
+        
+        public int getLength() {
+            return atts.size();
+        }
+        
+        public String getURI(int index) {
+            return "";
+        }
+
+        public String getLocalName(int index) {
+            return getByIndex(index).getKey();
+        }
+
+        public String getQName(int index) {
+            return getByIndex(index).getKey();
+        }
+
+        public String getType(int index) {
+            return "CDATA";
+        }
+
+        public int getIndex(String uri, String localName) {
+            return findIndex(localName);
+        }
+
+        public int getIndex(String qName) {
+            return findIndex(qName);
+        }
+
+        public String getType(String uri, String localName) {
+            return "CDATA";
+        }
+
+        public String getType(String qName) {
+            return "CDATA";
+        }
+
+        public String getValue(int index) {
+            return getByIndex(index).getValue();
+        }
+
+        public String getValue(String uri, String localName) {
+            return atts.get(localName);
+        }
+
+        public String getValue(String qName) {
+            return atts.get(qName);
+        }
+    }
+
+    private static String getRelativePath(File root, File current, File other) throws Exception {
+        if (current.equals(other)) {
+            return "";
+        }
+        
+        String rootPath = root.getCanonicalPath();
+        String currentPath = current.getCanonicalPath();
+        StringBuilder builder = new StringBuilder();
+        while (!rootPath.equals(currentPath)) {
+            current = current.getParentFile();
+            currentPath = current.getCanonicalPath();
+            builder.append("../");
+        }
+        
+        String otherPath = other.getCanonicalPath();
+        
+        if (rootPath.equals(otherPath)) {
+            // nothing to do
+        } else if (otherPath.startsWith(rootPath)) {
+            String name = otherPath.substring(rootPath.length() + 1);
+            builder.append(name);
+            builder.append("/");
+        } else {
+            throw new RuntimeException("Non-relative locations: " + rootPath + " " + otherPath);
+        }       
+        
+        return builder.toString();
+    }
+}
diff --git a/src/main/java/org/apache/cxf/cwiki/Page.java b/src/main/java/org/apache/cxf/cwiki/Page.java
new file mode 100644
index 0000000..5a684e1
--- /dev/null
+++ b/src/main/java/org/apache/cxf/cwiki/Page.java
@@ -0,0 +1,655 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.cwiki;
+
+
+import java.io.Serializable;
+import java.io.StringReader;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.datatype.XMLGregorianCalendar;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+
+import org.apache.cxf.common.util.StringUtils;
+import org.apache.cxf.helpers.DOMUtils;
+import org.ccil.cowan.tagsoup.Parser;
+
+/**
+ * 
+ */
+public class Page extends AbstractPage implements Serializable {
+    
+
+    private static final long serialVersionUID = 1L;
+    private static final Map<String, String> CODE_TYPE_MAP = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
+    static {
+        CODE_TYPE_MAP.put("applescript", "shBrushAppleScript.js");
+        CODE_TYPE_MAP.put("actionscript3", "shBrushAS3.js");
+        CODE_TYPE_MAP.put("as3", "shBrushAS3.js");
+        CODE_TYPE_MAP.put("bash", "shBrushBash.js");
+        CODE_TYPE_MAP.put("shell", "shBrushBash.js");
+        CODE_TYPE_MAP.put("coldfusion", "shBrushColdFusion.js");
+        CODE_TYPE_MAP.put("cpp", "shBrushCpp.js");
+        CODE_TYPE_MAP.put("c", "shBrushCpp.js");
+        CODE_TYPE_MAP.put("c#", "shBrushCSharp.js");
+        CODE_TYPE_MAP.put("c-sharp", "shBrushCSharp.js");
+        CODE_TYPE_MAP.put("csharp", "shBrushCSharp.js");
+        CODE_TYPE_MAP.put("css", "shBrushCss.js");
+        CODE_TYPE_MAP.put("delphi", "shBrushDelphi.js");
+        CODE_TYPE_MAP.put("pascal", "shBrushDelphi.js");
+        CODE_TYPE_MAP.put("diff", "shBrushDiff.js");
+        CODE_TYPE_MAP.put("patch", "shBrushDiff.js");
+        CODE_TYPE_MAP.put("pas", "shBrushDiff.js");
+        CODE_TYPE_MAP.put("erl", "shBrushErlang.js");
+        CODE_TYPE_MAP.put("erlang", "shBrushErlang.js");
+        CODE_TYPE_MAP.put("groovy", "shBrushGroovy.js");
+        CODE_TYPE_MAP.put("java", "shBrushJava.js");
+        CODE_TYPE_MAP.put("jfx", "shBrushJavaFX.js");
+        CODE_TYPE_MAP.put("javafx", "shBrushJavaFX.js");
+        CODE_TYPE_MAP.put("js", "shBrushJScript.js");
+        CODE_TYPE_MAP.put("jscript", "shBrushJScript.js");
+        CODE_TYPE_MAP.put("javascript", "shBrushJScript.js");
+        CODE_TYPE_MAP.put("perl", "shBrushPerl.js");
+        CODE_TYPE_MAP.put("pl", "shBrushPerl.js");
+        CODE_TYPE_MAP.put("php", "shBrushPhp.js");
+        CODE_TYPE_MAP.put("text", "shBrushPlain.js");
+        CODE_TYPE_MAP.put("plain", "shBrushPlain.js");
+        CODE_TYPE_MAP.put("none", "shBrushPlain.js");
+        CODE_TYPE_MAP.put("py", "shBrushPython.js");
+        CODE_TYPE_MAP.put("python", "shBrushPython.js");
+        CODE_TYPE_MAP.put("powershell", "shBrushPowerShell.js");
+        CODE_TYPE_MAP.put("ps", "shBrushPowerShell.js");
+        CODE_TYPE_MAP.put("posh", "shBrushPowerShell.js");
+        CODE_TYPE_MAP.put("ruby", "shBrushRuby.js");
+        CODE_TYPE_MAP.put("rails", "shBrushRuby.js");
+        CODE_TYPE_MAP.put("ror", "shBrushRuby.js");
+        CODE_TYPE_MAP.put("rb", "shBrushRuby.js");
+        CODE_TYPE_MAP.put("sass", "shBrushSass.js");
+        CODE_TYPE_MAP.put("scss", "shBrushSass.js");
+        CODE_TYPE_MAP.put("scala", "shBrushScala.js");
+        CODE_TYPE_MAP.put("sql", "shBrushSql.js");
+        CODE_TYPE_MAP.put("vb", "shBrushVb.js");
+        CODE_TYPE_MAP.put("vbnet", "shBrushVb.js");
+        CODE_TYPE_MAP.put("xml", "shBrushXml.js");
+        CODE_TYPE_MAP.put("xhtml", "shBrushXml.js");
+        CODE_TYPE_MAP.put("xslt", "shBrushXml.js");
+        CODE_TYPE_MAP.put("html", "shBrushXml.js");
+        CODE_TYPE_MAP.put("html/xml", "shBrushXml.js");
+    }
+    
+    final XMLGregorianCalendar modified;
+    final String parent;
+    final String spaceKey;
+    Map<String, String> attachments;
+    Set<String> includes;
+    Map<String, Integer> childrenOf;
+    boolean hasBlog;
+    Set<String> codeTypes;
+    
+    transient String renderedContent;
+    transient String renderedDivContent;
+    transient String divIdForContent;
+    
+    transient SiteExporter exporter;
+
+    public Page(Document doc, SiteExporter exp) throws Exception {
+        this(DOMUtils.getFirstElement(doc.getDocumentElement()), exp);
+    }
+    
+    public Page(Element root, SiteExporter exp) throws Exception {
+        super(root);
+        exporter = exp;
+        //org.apache.cxf.helpers.XMLUtils.printDOM(doc.getDocumentElement());
+
+        parent = DOMUtils.getChildContent(root, "parentId");
+        spaceKey = DOMUtils.getChildContent(root, "space");
+
+        String mod = DOMUtils.getChildContent(root, "modified");
+        modified = DatatypeFactory.newInstance().newXMLGregorianCalendar(mod);
+        modified.setMillisecond(0);
+        
+        String c = DOMUtils.getChildContent(root, "content");
+        if (c != null) {
+            if (exp.getAPIVersion() == 2) {
+                checkContentV2(c);
+            } else {
+                checkContentV1(c);                
+            }
+        }
+    }
+    /*
+     * Makes a shallow copy without any content
+     */
+    public Page(Page source) {
+        super(source);
+        this.modified = source.modified;
+        this.parent = source.parent;
+        this.spaceKey = source.spaceKey;
+        this.attachments = source.attachments;
+        this.includes = source.includes;
+        this.childrenOf = source.childrenOf;
+        this.exporter = source.exporter;
+        this.hasBlog = source.hasBlog;
+        this.codeTypes = source.codeTypes;
+    }
+    
+    private void checkContentV2(final String c) {
+        try {
+            //if ("Content Based Router".equals(title)) {
+            //    System.out.println(c);
+            //}
+            
+            XMLReader reader = new Parser();
+            reader.setFeature(Parser.namespacesFeature, true);
+            reader.setFeature(Parser.namespacePrefixesFeature, true);
+            reader.setProperty(Parser.schemaProperty, new org.ccil.cowan.tagsoup.HTMLSchema() {
+                {
+                    //problem with nested lists that the confluence {toc} macro creates
+                    elementType("ul", M_LI, M_BLOCK | M_LI, 0);
+                }
+            });
+            reader.setContentHandler(new V2ContentHandler(this));
+            reader.parse(new InputSource(new StringReader(c)));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void checkContentV1(String c) {
+        int idx = c.indexOf("{children");
+        while (idx != -1) {
+            if (childrenOf == null) {
+                childrenOf = new HashMap<String, Integer>();
+            }
+            idx += 9;
+            if (c.charAt(idx) != '}') {
+                // {children:page=Foo|...}
+                idx++;
+                int idx2 = c.indexOf('}', idx);
+                String paramString = c.substring(idx, idx2);
+                String params[] = paramString.split("\\||=");
+                String page = null;
+                int depth = 1;
+                for (int x = 0; x < params.length; x++) {
+                    if ("page".equals(params[x])) {
+                        page = params[x + 1];
+                        x++;
+                    } else if ("depth".equals(params[x])) {
+                        depth = Integer.parseInt(params[x + 1]);
+                        x++;
+                    }
+                }
+                childrenOf.put(page, depth);
+            } else {
+                childrenOf.put(title, 1);
+            }
+            idx = c.indexOf("{children", idx);
+        }
+        
+        idx = c.indexOf("{include:");
+        while (idx != -1) {
+            int idx2 = c.indexOf("}", idx);
+            String inc = c.substring(idx + 9, idx2);
+            if (includes == null) {
+                includes = new CopyOnWriteArraySet<String>();
+            }
+            includes.add(inc);
+            idx = c.indexOf("{include:", idx2);
+        }
+        idx = c.indexOf("{blog-posts");
+        if (idx != -1) {
+            hasBlog = true;
+        }
+        handleCode(c);
+    }
+
+    private void handleCode(String c) {
+        int idx = c.indexOf("{code");
+        while (idx != -1) {
+            String type = "java";
+            idx += 5;
+            if (c.charAt(idx) != '}') {
+                idx++;
+                int idx2 = c.indexOf('}', idx);
+                if (idx2 != -1) {
+                    String paramString = c.substring(idx, idx2);
+                    String params[] = paramString.split("\\||=");
+                    for (int x = 0; x < params.length; x++) {
+                        if ("type".equalsIgnoreCase(params[x])) {
+                            type = params[x + 1];
+                            x++;
+                        } else if (CODE_TYPE_MAP.containsKey(params[x].toLowerCase())) {
+                            type = params[x];
+                        }
+                    }
+                }
+            }
+
+            if (codeTypes == null) {
+                codeTypes = new ConcurrentSkipListSet<String>();
+            }
+            codeTypes.add(type);
+            idx = c.indexOf("{code", idx + 1);
+        } 
+        idx = c.indexOf("{snippet");
+        while (idx != -1) {
+            String type = "java";
+            idx += 8;
+            if (c.charAt(idx) != '}') {
+                idx++;
+                int idx2 = c.indexOf('}', idx);
+                if (idx2 != -1) {
+                    String paramString = c.substring(idx, idx2);
+                    String params[] = paramString.split("\\||=");
+                    for (int x = 0; x < params.length; x++) {
+                        if ("lang".equalsIgnoreCase(params[x])) {
+                            type = params[x + 1];
+                            x++;
+                        } else if (CODE_TYPE_MAP.containsKey(params[x].toLowerCase())) {
+                            type = params[x];
+                        }
+                    }
+                }
+            }
+
+            if (codeTypes == null) {
+                codeTypes = new ConcurrentSkipListSet<String>();
+            }
+            codeTypes.add(type);
+            idx = c.indexOf("{snippet", idx + 1);
+        } 
+    }
+    
+    public boolean hasChildrenOf(String t, int d) {
+        if (childrenOf == null) {
+            return false;
+        }
+        Integer i = childrenOf.get(t);
+        if (i == null) {
+            return false;
+        }
+        return d <= i;
+    }
+    
+    public boolean includesPage(String s) {
+        if (includes == null) {
+            return false;
+        }
+        return includes.contains(s);
+    }
+    
+
+    public String getParentId() {
+        return parent;
+    }
+
+    public XMLGregorianCalendar getModifiedTime() {
+        return modified;
+    }
+
+    public void setContent(String c) {
+        renderedContent = c;
+    }
+    
+    public String getContent() {
+        return renderedContent;
+    }
+
+    public String getSpaceKey() {
+        return spaceKey;
+    }
+
+    public void addAttachment(String aid, String filename) {
+        if (attachments == null) {
+            attachments = new HashMap<String, String>();
+        }
+        attachments.put(aid, filename);
+    }
+    public String getAttachmentFilename(String aid) {
+        if (attachments == null) {
+            return null;
+        }
+        return attachments.get(aid);
+    }
+
+    public void setContentForDivId(String divId, String content) {
+        renderedDivContent = content;
+        divIdForContent = divId;
+    }
+
+    public String getContentForDivId(String divId) {
+        if (divId == null) {
+            return renderedContent;
+        }
+        if (divId.equals(divIdForContent)) {
+            return renderedDivContent;
+        }
+        return null;
+    }
+
+    public String getLink() {
+        StringBuffer buffer = new StringBuffer();
+        buffer.append("<a href=\"");
+        buffer.append(url);
+        buffer.append("\" title=\"");
+        buffer.append(title);
+        buffer.append("\">");
+        buffer.append(title);
+        buffer.append("</a>");
+        return buffer.toString();
+    }
+
+    public Space getSpace() {
+        return SiteExporter.getSpace(spaceKey);
+    }
+
+    public boolean hasChildren() {
+        return exporter.hasChildren(this);
+    }
+
+    public List<Page> getChildren() {
+        return exporter.getChildren(this);
+    }
+
+    protected void setExporter(SiteExporter exporter) {
+        this.exporter = exporter;
+    }
+    
+    protected SiteExporter getExporter() {
+        return exporter;
+    }
+
+    public boolean hasBlog() {
+        return hasBlog;
+    }
+    
+    public boolean getHasCode() {
+        return hasCode(new HashMap<String, Boolean>());
+    }
+
+    public boolean hasCode(Map<String, Boolean> done) {
+        if (done.containsKey(getTitle())) {
+            return done.get(getTitle());
+        }
+        if (codeTypes != null && !codeTypes.isEmpty()) {
+            done.put(this.getTitle(), true);
+            return true;
+        }
+        if (includes != null) {
+            done.put(getTitle(), false);
+            for (String i : includes) {
+                try {
+                    Page p = exporter.findPage(i);
+                    if (p != null && p.hasCode(done)) {
+                        done.put(this.getTitle(), true);
+                        return true;
+                    }
+                } catch (Throwable e) {
+                    e.printStackTrace();
+                    System.out.println(done);
+                }
+            }
+        }
+        return false;
+    }
+    
+    public Set<String> getCodeScripts() throws Exception {
+        Set<String> scripts = new HashSet<String>();
+        if (codeTypes != null) {
+            for (String s : codeTypes) {
+                String sc = CODE_TYPE_MAP.get(s);
+                if (sc == null) {
+                    System.out.println("WARNING: no code highlighter for " + s);
+                } else {
+                    scripts.add(sc);
+                }
+            }
+        }
+        if (scripts.isEmpty()) {
+            scripts.add(CODE_TYPE_MAP.get("java"));
+            scripts.add(CODE_TYPE_MAP.get("plain"));
+        }
+        if (includes != null) {
+            for (String i : includes) {
+                try {
+                    Page p = exporter.findPage(i);
+                    if (p != null && p.getHasCode()) {
+                        scripts.addAll(p.getCodeScripts());
+                    } else if (p == null) {
+                        System.out.println("    Did not find page " + i);
+                    }
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return scripts;
+    }
+
+    
+    static class V2ContentHandler implements ContentHandler {
+        private final Page page;
+        
+        enum State {
+            NONE,
+            CHILDREN,
+            INCLUDE,
+            BLOG_POSTS,
+            CODE,
+        };
+        private State state = State.NONE;
+        private Map<String, String> params = new HashMap<String, String>();
+        private String paramName;
+        private boolean unmigrated;
+
+        V2ContentHandler(Page pg) {
+            page = pg;
+        }
+
+        public void setDocumentLocator(Locator locator) {
+        }
+
+        public void startDocument() throws SAXException {
+        }
+
+        public void endDocument() throws SAXException {
+        }
+
+        public void startPrefixMapping(String prefix, String uri) throws SAXException {
+        }
+
+        public void endPrefixMapping(String prefix) throws SAXException {
+        }
+
+        public void startElement(String uri, String localName, String qName, Attributes atts)
+            throws SAXException {
+            if ("macro".equals(localName) || "structured-macro".equals(localName)) {
+                String s = atts.getValue(uri, "name");
+                if ("children".equals(s)) {
+                    state = State.CHILDREN;
+                } else if ("include".equals(s)) {
+                    state = State.INCLUDE;
+                } else if ("blog-posts".equals(s)) {
+                    state = State.BLOG_POSTS;
+                } else if ("code".equals(s)) {
+                    state = State.CODE;
+                } else if ("snippet".equals(s)) {
+                    state = State.CODE;
+                } else if ("unmigrated-wiki-markup".equals(s)
+                    || "unmigrated-inline-wiki-markup".equals(s)) {
+                    if (!unmigrated) {
+                        System.out.println("WARNING: Page \"" + page.title + "\" (" 
+                            + page.spaceKey + ") has unmigrated wiki content.");
+                        unmigrated = true;
+                        //no idea what is in there, lets just turn on the code highlighting
+                        if (page.codeTypes == null) {
+                            page.codeTypes = new ConcurrentSkipListSet<String>();
+                        }
+                        page.codeTypes.add("java");
+                        page.codeTypes.add("xml");
+                        page.codeTypes.add("plain");
+                    }
+                } else {
+                    //System.out.println("Unknown macro: " + s);
+                }
+                params.clear();
+                paramName = null;
+            } else if ("parameter".equals(localName)) {
+                paramName = atts.getValue(uri, "name");
+            } else if ("default-parameter".equals(localName)) {
+                paramName = "default-parameter";
+            } else if ("page".equals(localName) && state == State.INCLUDE) {
+                for (int x = 0; x < atts.getLength(); x++) {
+                    if (atts.getLocalName(x).equals("content-title")
+                        && paramName != null) {
+                        params.put(paramName, atts.getValue(x));
+                    }
+                }
+            }
+        }
+
+        public void endElement(String uri, String localName, String qName) throws SAXException {
+            if ("macro".equals(localName) || "structured-macro".equals(localName)) {
+                switch (state) {
+                case CHILDREN: {
+                    String pageName = params.get("page");
+                    String depth = params.get("depth");
+                    if (depth == null || "".equals(depth.trim())) {
+                        depth = "1";
+                    }
+                    if (page.childrenOf == null) {
+                        page.childrenOf = new HashMap<String, Integer>();
+                    }
+                    if (pageName == null) {
+                        page.childrenOf.put(page.title, Integer.parseInt(depth));                    
+                    } else {
+                        page.childrenOf.put(pageName, Integer.parseInt(depth));
+                    }
+                    params.clear();
+                    state = State.NONE;
+                    break;
+                }
+                case INCLUDE: {
+                    if (page.includes == null) {
+                        page.includes = new CopyOnWriteArraySet<String>();
+                    }
+                    String inc = params.get("default-parameter");
+                    if (inc == null) {
+                        inc = params.get("title");
+                    }
+                    if (inc == null) {
+                        inc = params.get("");
+                    }
+                    if (inc == null) {
+                        System.out.println(page.title + ": Did not find an include name " + params);
+                    } else {
+                        page.includes.add(inc);
+                    }
+                    state = State.NONE;
+                    break;
+                }
+                case BLOG_POSTS:
+                    page.hasBlog = true;
+                    state = State.NONE;
+                    break;
+                case CODE: {
+                    if (page.codeTypes == null) {
+                        page.codeTypes = new ConcurrentSkipListSet<String>();
+                    }
+                    String lang = null;
+                    for (Map.Entry<String, String> ent : params.entrySet()) {
+                        if ("language".equals(ent.getKey())) {
+                            lang = ent.getValue();
+                        } else if (ent.getKey().contains(":")) {
+                            String parts[] = ent.getKey().split(":");
+                            for (String s : parts) {
+                                if (("title".equals(s) && !params.containsKey("title"))
+                                    || (!params.containsKey("language") 
+                                        && ("xml".equals(s) || "java".equals(s)))) {
+                                    System.out.println("WARNING Page " + page.title + " has a broken code block");
+                                }
+                            }
+                        } else if ("default-parameter".equals(ent.getKey())) {
+                            String s = ent.getValue();
+                            if ("xml".equals(s) || "java".equals(s)) {
+                                lang = s;
+                            }
+                        }
+                    }
+                    if (lang == null) {
+                        lang = params.get("");
+                        if (!StringUtils.isEmpty(lang)) {
+                            page.codeTypes.add("java");
+                        }
+                    }
+                    //System.out.println("l:  " + lang + "   " + params);
+                    if (StringUtils.isEmpty(lang)) {
+                        page.codeTypes.add("bash");
+                        lang = "java";
+                    }
+                    page.codeTypes.add(lang);
+                    state = State.NONE;
+                    break;
+                }                    
+                default:
+                    state = State.NONE;
+                    break;
+                }
+            } else if ("parameter".equals(localName)) {
+                paramName = null;
+            } else if ("default-parameter".equals(localName)) {
+                paramName = null;
+            }            
+        }
+
+        public void characters(char[] ch, int start, int length) throws SAXException {
+            if (paramName != null) {
+                params.put(paramName, new String(ch, start, length));
+            }
+        }
+
+        public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
+        }
+
+        public void processingInstruction(String target, String data) throws SAXException {
+        }
+
+        public void skippedEntity(String name) throws SAXException {
+        }
+    }
+    
+}
diff --git a/src/main/java/org/apache/cxf/cwiki/PageManager.java b/src/main/java/org/apache/cxf/cwiki/PageManager.java
new file mode 100644
index 0000000..779423c
--- /dev/null
+++ b/src/main/java/org/apache/cxf/cwiki/PageManager.java
@@ -0,0 +1,63 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.cwiki;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 
+ */
+public class PageManager {
+
+    private SiteExporter exporter;
+    private String dir;
+    private Map<String, Page> pages = new HashMap<String, Page>();
+    
+    public PageManager(SiteExporter exporter) {
+        this.exporter = exporter;
+    }
+    
+    public void setDirectory(String d) {
+        this.dir = d;
+    }
+    
+    public Page getPage(String spaceKey, String title) throws Exception {
+        // XXX: spaceKey must match exporter.getSpace().getKey()
+        
+        // lookup cached page
+        Page cachedPage = pages.get(title);
+        if (cachedPage == null) {
+            // lookup real page       
+            Page page = exporter.findPage(title);
+            if (page != null) {
+                cachedPage = new Page(page);                
+                cachedPage.directory = dir;
+                exporter.loadPageContent(cachedPage, null, null);
+                pages.put(title, cachedPage);
+            } else {
+                System.err.println("Page not found: " + title);
+            }
+        }
+        
+        return cachedPage;
+    }
+    
+}
diff --git a/src/main/java/org/apache/cxf/cwiki/Renderer.java b/src/main/java/org/apache/cxf/cwiki/Renderer.java
new file mode 100644
index 0000000..317189f
--- /dev/null
+++ b/src/main/java/org/apache/cxf/cwiki/Renderer.java
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.cwiki;
+
+/**
+ * 
+ */
+public class Renderer {
+
+    protected SiteExporter exporter;
+    
+    public Renderer(SiteExporter exporter) {
+        this.exporter = exporter;
+    }
+    
+    public String convertWikiToXHtml(Object context, String content) {
+        if (content != null) {
+            int start = 0;
+            if (content.startsWith("<div")) {
+                int pos = content.indexOf(">", 1);
+                if (pos != -1) {
+                    start = pos + 1;
+                }
+            }
+            int end = content.length();
+            if (content.endsWith("</div>")) {
+                end -= "</div>".length();
+            }
+            content = content.substring(start, end);
+        }
+        
+        return content;
+    }
+    
+}
diff --git a/src/main/java/org/apache/cxf/cwiki/SiteExporter.java b/src/main/java/org/apache/cxf/cwiki/SiteExporter.java
new file mode 100644
index 0000000..b6c4def
--- /dev/null
+++ b/src/main/java/org/apache/cxf/cwiki/SiteExporter.java
@@ -0,0 +1,1313 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.cwiki;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.datatype.XMLGregorianCalendar;
+import javax.xml.namespace.QName;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.ws.AsyncHandler;
+import javax.xml.ws.Dispatch;
+import javax.xml.ws.Response;
+import javax.xml.ws.Service;
+import javax.xml.ws.soap.SOAPBinding;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import org.xml.sax.ContentHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.XMLReader;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+
+import org.apache.cxf.clustering.FailoverFeature;
+import org.apache.cxf.clustering.RetryStrategy;
+import org.apache.cxf.common.classloader.ClassLoaderUtils;
+import org.apache.cxf.common.util.Base64Utility;
+import org.apache.cxf.feature.Feature;
+import org.apache.cxf.helpers.CastUtils;
+import org.apache.cxf.helpers.DOMUtils;
+import org.apache.cxf.helpers.FileUtils;
+import org.apache.cxf.helpers.IOUtils;
+import org.apache.cxf.interceptor.LoggingInInterceptor;
+import org.apache.cxf.interceptor.LoggingOutInterceptor;
+import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean;
+import org.apache.cxf.staxutils.StaxUtils;
+import org.apache.cxf.transport.http.HTTPConduit;
+import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.runtime.resource.loader.URLResourceLoader;
+import org.ccil.cowan.tagsoup.Parser;
+import org.ccil.cowan.tagsoup.XMLWriter;
+
+import application.ContentResource;
+
+/**
+ * 
+ */
+public class SiteExporter implements Runnable {
+
+    static final String HOST = "https://cwiki.apache.org";
+    static final String ROOT = HOST + "/confluence";
+    static final String RPC_ROOT = "/rpc/soap-axis/confluenceservice-v";    
+    static final String SOAPNS = "http://soap.rpc.confluence.atlassian.com";
+    static final String REST_API = ROOT + "/rest/api";
+    
+    static final String SEPARATOR = "&nbsp;&gt;&nbsp;";
+    
+    
+    
+    static boolean debug;
+    static String userName = "cxf-export-user";
+    static String password;
+    
+    static int apiVersion = 1;
+    
+    static boolean svn;
+    static boolean commit;
+    static StringBuilder svnCommitMessage = new StringBuilder();
+    
+    static File rootOutputDir = new File(".");
+    static String loginToken;
+    static Dispatch<Document> dispatch;
+    static ContentResource contentResource;
+    static AtomicInteger asyncCount = new AtomicInteger();
+    static Map<String, Space> spaces = new ConcurrentHashMap<String, Space>();
+    static List<SiteExporter> siteExporters;
+
+    Map<String, Page> pages = new ConcurrentHashMap<String, Page>();
+    Collection<Page> modifiedPages = new ConcurrentLinkedQueue<Page>();
+    Set<String> globalPages = new CopyOnWriteArraySet<String>();
+    
+    Map<String, BlogEntrySummary> blog = new ConcurrentHashMap<String, BlogEntrySummary>();
+    Set<BlogEntrySummary> modifiedBlog = new CopyOnWriteArraySet<BlogEntrySummary>();
+    
+
+    String spaceKey = "CXF";
+    String pageCacheFile = "pagesConfig.obj";
+    String templateName = "template/template.vm";
+    String mainDivClass;
+    boolean forceAll;
+    String breadCrumbRoot;
+    
+    File outputDir = rootOutputDir;
+
+    Template template;
+    Space space;
+    
+
+    public SiteExporter(String fileName, boolean force) throws Exception {
+        forceAll = force;
+        
+        Properties props = new Properties();
+        props.load(new FileInputStream(fileName));
+        
+        if (props.containsKey("spaceKey")) {
+            spaceKey = props.getProperty("spaceKey");
+        }
+        if (props.containsKey("pageCacheFile")) {
+            pageCacheFile = props.getProperty("pageCacheFile");
+        }
+        if (props.containsKey("templateName")) {
+            templateName = props.getProperty("templateName");
+        }
+        if (props.containsKey("outputDir")) {
+            outputDir = new File(rootOutputDir, props.getProperty("outputDir"));
+        }
+        if (props.containsKey("mainDivClass")) {
+            mainDivClass = props.getProperty("mainDivClass");
+        }
+        if (props.containsKey("breadCrumbRoot")) {
+            breadCrumbRoot = props.getProperty("breadCrumbRoot");
+        }
+        if (props.containsKey("globalPages")) {
+            String globals = props.getProperty("globalPages");
+            String[] pgs = globals.split(",");
+            globalPages.addAll(Arrays.asList(pgs));
+        }
+        
+        props = new Properties();
+        String clzName = URLResourceLoader.class.getName();
+        props.put("resource.loader", "url");
+        props.put("url.resource.loader.class", clzName);
+        props.put("url.resource.loader.root", "");
+        
+        VelocityEngine engine = new VelocityEngine();
+        engine.init(props);
+            
+        URL url = ClassLoaderUtils.getResource(templateName, this.getClass());
+        if (url == null) {
+            File file = new File(templateName);
+            if (file.exists()) {
+                url = file.toURI().toURL();
+            } else {
+                //try relative to this cfg file
+                file = new File(fileName);
+                file = new File(file.getParentFile().toURI().resolve(templateName));
+                if (file.exists()) {
+                    url = file.toURI().toURL();
+                }
+            }
+        }
+        if (url == null) {
+            File file = new File(fileName);
+            file = new File(file.getParentFile().toURI().resolve(templateName));
+            System.err.println("Could not find " + templateName + "   " + fileName);
+            System.err.println("               " + file.toURI().toURL());
+        }
+        template = engine.getTemplate(url.toURI().toString());
+               
+        outputDir.mkdirs();
+    }
+    
+    public static synchronized ContentResource getContentResource() {
+        if (contentResource == null) {
+            FailoverFeature failover = new FailoverFeature();
+            RetryStrategy rs = new RetryStrategy();
+            rs.setMaxNumberOfRetries(25);
+            List<String> alternateAddresses = new ArrayList<String>();
+            alternateAddresses.add(REST_API);
+            rs.setAlternateAddresses(alternateAddresses);
+            failover.setStrategy(rs);
+            JAXRSClientFactoryBean bean = new JAXRSClientFactoryBean();
+            bean.setAddress(REST_API);
+            
+            bean.setServiceClass(ContentResource.class);
+            List<Feature> features = new ArrayList<Feature>();
+            
+            
+            features.add(failover);
+            bean.setFeatures(features);
+            bean.setUsername(userName);
+            bean.setPassword(password);
+            contentResource = bean.create(ContentResource.class);
+        }
+        return contentResource;
+    }
+    public static synchronized Dispatch<Document> getDispatch() {
+        if (dispatch == null) {
+            
+            FailoverFeature failover = new FailoverFeature();
+            RetryStrategy rs = new RetryStrategy();
+            rs.setMaxNumberOfRetries(25);
+            List<String> alternateAddresses = new ArrayList<String>();
+            alternateAddresses.add(ROOT + RPC_ROOT + apiVersion);
+            alternateAddresses.add(ROOT + RPC_ROOT + apiVersion);
+            alternateAddresses.add(ROOT + RPC_ROOT + apiVersion);
+            rs.setAlternateAddresses(alternateAddresses);
+            failover.setStrategy(rs);
+            
+            
+            Service service = Service.create(new QName(SOAPNS, "Service"), failover);
+            service.addPort(new QName(SOAPNS, "Port"), 
+                            SOAPBinding.SOAP11HTTP_BINDING,
+                            ROOT + RPC_ROOT + apiVersion);
+    
+            dispatch = service.createDispatch(new QName(SOAPNS, "Port"), 
+                                              Document.class, Service.Mode.PAYLOAD);
+            if (debug) {
+                ((org.apache.cxf.jaxws.DispatchImpl<?>)dispatch).getClient()
+                    .getEndpoint().getInInterceptors().add(new LoggingInInterceptor());
+                ((org.apache.cxf.jaxws.DispatchImpl<?>)dispatch).getClient()
+                    .getEndpoint().getOutInterceptors().add(new LoggingOutInterceptor());
+            }
+            HTTPConduit c = (HTTPConduit)((org.apache.cxf.jaxws.DispatchImpl<?>)dispatch)
+                .getClient().getConduit();
+            HTTPClientPolicy clientPol = c.getClient();
+            if (clientPol == null) {
+                clientPol = new HTTPClientPolicy();
+            }
+            //CAMEL has a couple of HUGE HUGE pages that take a long time to render
+            clientPol.setReceiveTimeout(5 * 60 * 1000);
+            c.setClient(clientPol);
+            
+        }
+        return dispatch;
+    }
+    
+    public void run() {
+        try {
+            render();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+    
+    public void forcePage(String s) throws Exception {
+        Page p = findPage(s);
+        if (p != null) {
+            pages.remove(p.getId());
+            if (!modifiedPages.contains(p)) {
+                modifiedPages.add(p);
+            }
+        }
+    }
+    
+    /**
+     * @return true if some pages have changed - rendering is needed
+     * @throws Exception
+     */
+    public boolean initialize() throws Exception {
+        if (!forceAll) {
+            loadCache();
+        }
+        
+        // debug stuff, force regen of a page
+        //forcePage("Navigation");
+        //forcePage("Index");
+        //forcePage("JavaDocs");
+        //forcePage("DOSGi Architecture");
+        //forcePage("Book In One Page");
+        //forcePage("Security");
+        //forcePage("FAQ");
+        
+        /*
+        if (modifiedPages.isEmpty() && checkRSS()) {
+            System.out.println("(" + spaceKey + ") No changes detected from RSS");
+            return false;
+        }
+        */
+
+        doLogin();
+        checkVersion();
+        getSpace();
+        if ("-space-".equals(breadCrumbRoot)) {
+            breadCrumbRoot = space.getName();
+        }
+        loadBlog();
+        loadPages();
+        
+        return true;
+    }
+        
+    private void checkVersion() throws ParserConfigurationException, IOException {
+        Document doc = DOMUtils.createDocument();
+        Element el = doc.createElementNS(SOAPNS, "ns1:getServerInfo");
+        Element el2 = doc.createElement("in0");
+        el.appendChild(el2);
+        el2.setTextContent(loginToken);
+        doc.appendChild(el);
+
+        doc = getDispatch().invoke(doc);
+        el = DOMUtils.getFirstElement(DOMUtils.getFirstElement(doc.getDocumentElement()));
+        while (el != null) {
+            if ("majorVersion".equals(el.getLocalName())) {
+                String major = DOMUtils.getContent(el);
+                if (Integer.parseInt(major) >= 5) {
+                    apiVersion = 2;
+                    ((java.io.Closeable)dispatch).close();
+                    dispatch = null;
+                }
+            }
+              
+            el = DOMUtils.getNextElement(el);
+        }
+    }
+
+    protected void render() throws Exception {
+        for (Page p : modifiedPages) {
+            if (globalPages.contains(p.getTitle())) {
+                modifiedPages.clear();
+                modifiedPages.addAll(pages.values());
+                break;
+            }
+        }
+        
+        if (forceAll) {
+            modifiedPages.clear();
+            modifiedPages.addAll(pages.values());
+            
+            modifiedBlog.clear();
+            modifiedBlog.addAll(blog.values());
+        }
+        if (!modifiedBlog.isEmpty()) {
+            //blogs changed, see if any pages have blogs
+            for (Page p : pages.values()) {
+                if (p.hasBlog() && !modifiedPages.contains(p)) {
+                    modifiedPages.add(p);
+                }
+            }
+        }
+        if (!modifiedPages.isEmpty() || !modifiedBlog.isEmpty()) {
+            renderBlog();
+            renderPages();
+            saveCache();
+        }
+    }
+
+
+    public boolean checkRSS() throws Exception {
+        if (forceAll || pages == null || pages.isEmpty()) {
+            return false;
+        }
+        URL url = new URL(ROOT + "/createrssfeed.action?types=page&types=blogpost&types=mail&"
+                          //+ "types=comment&"  //cannot handle comment updates yet
+                          + "types=attachment&statuses=created&statuses=modified"
+                          + "&spaces=" + spaceKey + "&rssType=atom&maxResults=20&timeSpan=2"
+                          + "&publicFeed=true");
+        InputStream ins = url.openStream();
+        Document doc = StaxUtils.read(ins);
+        ins.close();
+        List<Element> els = DOMUtils.getChildrenWithName(doc.getDocumentElement(),
+                                                        "http://www.w3.org/2005/Atom", 
+                                                        "entry");
+        // XMLUtils.printDOM(doc);
+        for (Element el : els) {
+            Element e2 = DOMUtils.getFirstChildWithName(el, "http://www.w3.org/2005/Atom", "updated");
+            String val = DOMUtils.getContent(e2);
+            XMLGregorianCalendar cal = DatatypeFactory.newInstance().newXMLGregorianCalendar(val);
+            e2 = DOMUtils.getFirstChildWithName(el, "http://www.w3.org/2005/Atom", "title");
+            String title = DOMUtils.getContent(e2);
+            
+            Page p = findPage(title);
+            if (p != null) {
+                //found a modified page - need to rebuild
+                if (cal.compare(p.getModifiedTime()) > 0) {
+                    System.out.println("(" + spaceKey + ") Changed page found: " + title);
+                    return false;
+                }
+            } else {
+                BlogEntrySummary entry = findBlogEntry(title);
+                if (entry != null) {
+                    // we don't have modified date so just assume it's modified
+                    // we'll use version number to actually figure out if page is modified or not
+                    System.out.println("(" + spaceKey + ") Possible changed blog page found: " + title);
+                    return false;
+                } else {
+                    System.out.println("(" + spaceKey + ") Did not find page for: " + title);
+                    return false;
+                }
+            }
+        }
+        
+        return true;
+    }
+
+    private void saveCache() throws Exception {
+        File file = new File(rootOutputDir, pageCacheFile);
+        file.getParentFile().mkdirs();
+        FileOutputStream fout = new FileOutputStream(file);
+        ObjectOutputStream oout = new ObjectOutputStream(fout);
+        oout.writeObject(pages);
+        oout.writeObject(blog);
+        oout.close();
+    }
+
+    private void renderPages() throws Exception {
+        PageManager pageManager = new PageManager(this);
+        Renderer renderer = new Renderer(this);
+        
+        int total = modifiedPages.size();
+        int count = 0;
+        for (Page p : modifiedPages) {
+            count++;
+            System.out.println("(" + spaceKey + ") Rendering " + p.getTitle() 
+                               + "    (" + count + "/" + total + ")");
+            loadAttachments(p);
+            
+            try {
+                loadPageContent(p, null, null);
+                
+                VelocityContext ctx = new VelocityContext();
+                ctx.put("autoexport", this);
+                ctx.put("page", p);
+                ctx.put("body", p.getContent());
+                ctx.put("confluenceUri", ROOT);
+                ctx.put("pageManager", pageManager);
+                ctx.put("renderer", renderer);
+                ctx.put("exporter", this);
+                
+                File file = new File(outputDir, p.createFileName());
+                boolean isNew = !file.exists();
+                
+                FileWriter writer = new FileWriter(file);
+                ctx.put("out", writer);
+                template.merge(ctx, writer);
+                writer.close();
+                if (isNew) {
+                    //call "svn add"
+                    callSvn("add", file.getAbsolutePath());
+                    svnCommitMessage.append("Adding: " + file.getName() + "\n");
+                } else {
+                    svnCommitMessage.append("Modified: " + file.getName() + "\n");                
+                }
+                
+                p.setContent(null);
+            } catch (Exception e) {
+                System.out.println("Could not render page " + p.getTitle() + " due to " + e.getMessage());
+                e.printStackTrace();
+            }
+
+        }
+    }
+    
+    private void renderBlog() throws Exception {
+        PageManager pageManager = new PageManager(this);
+        Renderer renderer = new Renderer(this);
+        
+        int total = modifiedBlog.size();
+        int count = 0;
+        for (BlogEntrySummary entry : modifiedBlog) {
+            count++;
+            System.out.println("(" + spaceKey + ") Rendering Blog Entry " + entry.getTitle() 
+                               + "    (" + count + "/" + total + ")");
+            
+            try {
+                loadAttachments(entry);
+                String body = renderPage(entry);
+                body = updateContentLinks(entry, body, null, mainDivClass);
+                
+                pageManager.setDirectory(entry.getDirectory());
+                
+                VelocityContext ctx = new VelocityContext();
+                ctx.put("autoexport", this);
+                ctx.put("page", entry);
+                ctx.put("body", body);
+                ctx.put("confluenceUri", ROOT);
+                ctx.put("pageManager", pageManager);
+                ctx.put("renderer", renderer);
+                ctx.put("exporter", this);
+                ctx.put("isBlogEntry", Boolean.TRUE);
+                
+                File file = new File(outputDir, entry.getPath());
+                file.getParentFile().mkdirs();
+                boolean isNew = !file.exists();
+                
+                FileWriter writer = new FileWriter(file);
+                ctx.put("out", writer);
+                template.merge(ctx, writer);
+                writer.close();
+                if (isNew) {
+                    //call "svn add"
+                    callSvn("add", file.getAbsolutePath());
+                    svnCommitMessage.append("Adding: " + file.getName() + "\n");
+                } else {
+                    svnCommitMessage.append("Modified: " + file.getName() + "\n");                
+                }
+            } catch (Exception e) {
+                System.out.println("Could not render blog " + entry.getTitle() + " due to " + e.getMessage());
+                e.printStackTrace();
+            }
+        }
+    }
+    
+    void callSvn(String ... commands) throws Exception {
+        callSvn(outputDir, commands);
+    }
+    static void callSvn(File dir, String ... commands) throws Exception {
+        if (svn) {
+            List<String> cmds = new ArrayList<String>();
+            cmds.add("svn");
+            cmds.add("--non-interactive");
+            cmds.addAll(Arrays.asList(commands));
+            Process p = Runtime.getRuntime().exec(cmds.toArray(new String[cmds.size()]),
+                                                  new String[0], dir);
+            if (p.waitFor() != 0) {
+                IOUtils.copy(p.getErrorStream(), System.err);
+            }
+        }
+    }
+    
+    private void loadAttachments(AbstractPage p) throws Exception {
+        Document doc = DOMUtils.createDocument();
+        Element el = doc.createElementNS(SOAPNS, "ns1:getAttachments");
+        Element el2 = doc.createElement("in0");
+        el.appendChild(el2);
+        el2.setTextContent(loginToken);
+        el2 = doc.createElement("in1");
+        el.appendChild(el2);
+        el2.setTextContent(p.getId());
+        el.appendChild(el2);
+        doc.appendChild(el);
+
+        doc = getDispatch().invoke(doc);
+        el = DOMUtils.getFirstElement(DOMUtils.getFirstElement(doc.getDocumentElement()));
+        while (el != null) {
+            try {
+                String filename = DOMUtils.getChildContent(el, "fileName");
+                String durl = DOMUtils.getChildContent(el, "url");
+                String aid = DOMUtils.getChildContent(el, "id");
+                
+                p.addAttachment(aid, filename);
+                
+                String dirName = p.getPath();
+                dirName = dirName.substring(0, dirName.lastIndexOf(".")) + ".data";
+                File file = new File(outputDir, dirName);
+                if (!file.exists()) {
+                    callSvn("mkdir", file.getAbsolutePath());
+                    file.mkdirs();
+                }
+                file = new File(file, filename);
+                boolean exists = file.exists();
+                FileOutputStream out = new FileOutputStream(file);
+                URL url = new URL(durl);
+                InputStream ins = url.openStream();
+                IOUtils.copy(ins, out);
+                out.close();
+                ins.close();
+                if (!exists) {
+                    callSvn("add", file.getAbsolutePath());
+                    svnCommitMessage.append("Added: " + dirName + "/" + file.getName() + "\n");
+                } else {
+                    svnCommitMessage.append("Modified: " + dirName + "/" + file.getName() + "\n");
+                }
+                if (filename.indexOf(' ') != -1) {
+                    filename = filename.replace(' ', '-');
+                    file = new File(outputDir, dirName);
+                    File f2 = new File(file, filename);
+                    exists = f2.exists();
+                    out = new FileOutputStream(f2);
+                    url = new URL(durl);
+                    ins = url.openStream();
+                    IOUtils.copy(ins, out);
+                    out.close();
+                    ins.close();
+                    if (!exists) {
+                        callSvn("add", f2.getAbsolutePath());
+                        svnCommitMessage.append("Added: " + dirName + "/" + f2.getName() + "\n");
+                    } else {
+                        svnCommitMessage.append("Modified: " + dirName + "/" + f2.getName() + "\n");
+                    }
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            el = DOMUtils.getNextElement(el);
+        }
+    }
+    String loadUserImage(AbstractPage p, String href) throws Exception {
+        return loadPageBinaryData(p, href, "userimage", true);
+    }
+    String loadThumbnail(AbstractPage p, String href) throws Exception {
+        return loadPageBinaryData(p, href, "thumbs", false);
+    }
+    String loadPageBinaryData(AbstractPage p, String href, String type, boolean auth) throws Exception {
+        String filename = href.substring(href.lastIndexOf('/') + 1);
+        filename = filename.replace(' ', '_');
+        if (filename.indexOf('?') != -1) {
+            filename = filename.substring(0, filename.indexOf('?'));
+        }
+        
+        String dirName = p.getPath();
+        dirName = dirName.substring(0, dirName.lastIndexOf(".")) + "." + type;
+        File file = new File(outputDir, dirName);
+        if (!file.exists()) {
+            callSvn("mkdir", file.getAbsolutePath());
+            file.mkdirs();
+        }
+        file = new File(file, filename);
+        boolean exists = file.exists();
+        FileOutputStream out = new FileOutputStream(file);
+        if (auth) {
+            if (href.indexOf('?') != -1) {
+                href += "&os_authType=basic";
+            } else {
+                href += "?os_authType=basic";
+            }
+        }
+        URL url = new URL(HOST + href);
+        URLConnection con = url.openConnection();
+        if (auth) {
+            con.addRequestProperty("Authorization", getBasicAuthHeader());
+        }
+        InputStream ins = con.getInputStream();
+        IOUtils.copy(ins, out);
+        out.close();
+        ins.close();
+        if (!exists) {
+            callSvn("add", file.getAbsolutePath());
+            svnCommitMessage.append("Added: " + dirName + "/" + file.getName() + "\n");                
+        } else {
+            svnCommitMessage.append("Modified: " + dirName + "/" + file.getName() + "\n");
+        }
+        return file.getName();
+    }
+    public String getBasicAuthHeader() {
+        String userAndPass = userName + ":" + password;
+        try {
+            return "Basic " + Base64Utility.encode(userAndPass.getBytes("ISO-8859-1"));
+        } catch (UnsupportedEncodingException e) {
+            return "Basic";
+        }
+    }
+    public Page findPage(String title) throws Exception {
+        return (Page) findByTitle(title, pages.values());
+    }
+    
+    public Page findPageByURL(String url) throws Exception {
+        return (Page) findByURL(url, pages.values());
+    }
+    
+    public Page findPageByID(String id) {
+        for (Page p : pages.values()) {
+            if (p.getId().equals(id)) {
+                return p;
+            }
+        }
+        return null;
+    }
+
+    public String breadcrumbs(BlogEntrySummary page) {
+        StringBuffer buffer = new StringBuffer();
+        if (breadCrumbRoot != null) {
+            buffer.append("<a href=\"");
+            buffer.append("../../../index.html");
+            buffer.append("\">");
+            buffer.append(breadCrumbRoot);
+            buffer.append("</a>");
+            buffer.append(SEPARATOR);
+        } else {
+            buffer.append("<a href=\"../../../index.html\">Index</a>");
+            buffer.append(SEPARATOR);
+        }
+        XMLGregorianCalendar published = page.getPublished();
+        buffer.append(String.valueOf(published.getYear()));
+        buffer.append(SEPARATOR);
+        if (published.getMonth() < 10) {
+            buffer.append("0");
+        } 
+        buffer.append(String.valueOf(published.getMonth()));
+        buffer.append(SEPARATOR);
+        if (published.getDay() < 10) {
+            buffer.append("0");
+        } 
+        buffer.append(String.valueOf(published.getDay()));
+        buffer.append(SEPARATOR);
+        buffer.append("<a href=\"");
+        buffer.append(page.createFileName());
+        buffer.append("\">");
+        buffer.append(page.getTitle());
+        buffer.append("</a>");
+        return buffer.toString();
+    }
+    
+    public String breadcrumbs(Page page) {
+        StringBuffer buffer = new StringBuffer();
+        List<Page> p = new LinkedList<Page>();
+        String parentId = page.getParentId();
+        Page parent = pages.get(parentId);
+        while (parent != null) {
+            p.add(0, parent);
+            parentId = parent.getParentId();
+            parent = pages.get(parentId);
+        }
+        if (breadCrumbRoot != null) {
+            buffer.append("<a href=\"");
+            buffer.append("index.html");
+            buffer.append("\">");
+            buffer.append(breadCrumbRoot);
+            buffer.append("</a>");
+            buffer.append(SEPARATOR);
+        }
+        for (Page p2 : p) {
+            buffer.append("<a href=\"");
+            buffer.append(p2.createFileName());
+            buffer.append("\">");
+            buffer.append(p2.getTitle());
+            buffer.append("</a>");
+            buffer.append(SEPARATOR);
+        }
+        buffer.append("<a href=\"");
+        buffer.append(page.createFileName());
+        buffer.append("\">");
+        buffer.append(page.getTitle());
+        buffer.append("</a>");
+
+        return buffer.toString();
+    }
+    
+    public String getPageContent(String title, String divId) throws Exception {
+        Page p = findPage(title);
+        String s = p.getContentForDivId(divId);
+        if (s == null) {
+            s = loadPageContent(p, divId, null);
+        }
+        return s;
+    }
+    public String getPageContent(String title, String divId, String cls) throws Exception {
+        Page p = findPage(title);
+        String s = p.getContentForDivId(divId);
+        if (s == null) {
+            s = loadPageContent(p, divId, cls);
+        }
+        return s;
+    }
+    public String getPageContent(String title) throws Exception {
+        Page p = findPage(title);
+        String s = p.getContent();
+        if (s == null) {
+            loadPageContent(p, null, null);
+        }
+        return p.getContent();
+    }
+    protected String loadPageContent(Page p, String divId, String divCls) throws Exception {
+        String content = renderPage(p);
+        content = updateContentLinks(p, content, divId, 
+                                     divCls == null && divId == null ? mainDivClass : divCls);
+        if (divId == null) {
+            p.setContent(content);
+        } else {
+            p.setContentForDivId(divId, content);
+        }
+        return content;
+    }
+
+    private String renderPage(AbstractPage p) throws ParserConfigurationException, IOException {
+        ContentResource content = getContentResource();
+        InputStream ins = content.getContentById(p.getId(), null, null, "body.export_view")
+                .readEntity(InputStream.class);
+        
+        JsonParser parser = new JsonFactory().createParser(ins);
+        JsonToken tok = parser.nextToken();
+        boolean inExportView = false;
+        while (tok != null) {
+            if (tok == JsonToken.FIELD_NAME) {
+                if (parser.getCurrentName().equals("export_view")) {
+                    inExportView = true;
+                }
+            } else if (tok == JsonToken.VALUE_STRING && inExportView && parser.getCurrentName().equals("value")) {
+                return "<div id='ConfluenceContent'>" + parser.getText() + "</div>";
+            }
+            tok = parser.nextToken();
+        }
+        System.out.println("No text for page \"" + p.getTitle() + "\"");
+        return "";
+    }
+
+    public String unwrap(String v) throws Exception {
+        if (v == null) {
+            return null;
+        }
+        return v.trim().replaceFirst("^<div[^>]*>", "").replaceFirst("</div>$", "");
+    }
+
+    private static synchronized void doLogin() throws Exception {
+        if (loginToken == null) {
+            Document doc = DOMUtils.createDocument();
+            Element el = doc.createElementNS(SOAPNS, "ns1:login");
+            Element el2 = doc.createElement("in0");
+            
+            if (userName == null) {
+                System.out.println("Enter username: ");
+                el2.setTextContent(System.console().readLine());
+            } else {
+                el2.setTextContent(userName);
+            }
+            el.appendChild(el2);
+            el2 = doc.createElement("in1");
+            el.appendChild(el2);
+            if (password == null) {
+                System.out.println("Enter password: ");
+                el2.setTextContent(new String(System.console().readPassword()));
+            } else {
+                el2.setTextContent(password);
+            }
+            doc.appendChild(el);
+            doc = getDispatch().invoke(doc);
+            loginToken = doc.getDocumentElement().getFirstChild().getTextContent();
+        }
+    }
+
+    public void loadCache() throws Exception {
+        File file = new File(rootOutputDir, pageCacheFile);
+        if (file.exists()) {
+            try {
+                FileInputStream fin = new FileInputStream(file);
+                ObjectInputStream oin = new ObjectInputStream(fin);
+                pages = CastUtils.cast((Map<?, ?>)oin.readObject());
+                blog = CastUtils.cast((Map<?, ?>)oin.readObject());
+                oin.close();
+                
+                for (Page p : pages.values()) {
+                    p.setExporter(this);
+                }
+            } catch (Throwable t) {
+                //invalid cache, punt
+                pages.clear();
+                blog.clear();
+            }
+        }
+    }
+    
+    public int getBlogVersion(String pageId) throws Exception {
+        Document doc = DOMUtils.newDocument();
+        Element el = doc.createElementNS(SOAPNS, "ns1:getBlogEntry");
+        Element el2 = doc.createElement("in0");
+        el.appendChild(el2);
+        el2.setTextContent(loginToken);
+        el2 = doc.createElement("in1");
+        el.appendChild(el2);
+        el2.setTextContent(pageId);
+        doc.appendChild(el);
+        doc = getDispatch().invoke(doc);
+        
+        Node nd = doc.getDocumentElement().getFirstChild();
+        
+        String version = DOMUtils.getChildContent(nd, "version");
+        return Integer.parseInt(version);
+    }
+    
+    public void loadBlog() throws Exception {
+        System.out.println("Loading Blog entries for " + spaceKey);
+        Document doc = DOMUtils.createDocument();
+        Element el = doc.createElementNS(SOAPNS, "ns1:getBlogEntries");
+        Element el2 = doc.createElement("in0");
+        el.appendChild(el2);
+        el2.setTextContent(loginToken);
+        el2 = doc.createElement("in1");
+        el.appendChild(el2);
+        el2.setTextContent(spaceKey);
+        doc.appendChild(el);
+        doc = getDispatch().invoke(doc);
+        
+        Map<String, BlogEntrySummary> oldBlog = new ConcurrentHashMap<String, BlogEntrySummary>(blog);
+        
+        Node nd = doc.getDocumentElement().getFirstChild().getFirstChild();
+        while (nd != null) {
+            if (nd instanceof Element) {
+                BlogEntrySummary entry = new BlogEntrySummary((Element)nd);
+                entry.setVersion(getBlogVersion(entry.id));
+                BlogEntrySummary oldEntry = blog.put(entry.getId(), entry);
+                System.out.println("Found Blog entry for " + entry.getTitle() + " " + entry.getPath());
+
+                if (oldEntry == null || oldEntry.getVersion() != entry.getVersion()) {
+                    System.out.println("   and it's modified");
+                    modifiedBlog.add(entry);
+                } else {
+                    System.out.println("   but it's not modified");
+                }
+                oldBlog.remove(entry.getId());
+            }
+            nd = nd.getNextSibling();
+        }
+        
+        for (String id : oldBlog.keySet()) {
+            //these pages have been deleted
+            BlogEntrySummary p = blog.remove(id);
+            File file = new File(outputDir, p.getPath());
+            if (file.exists()) {
+                callSvn("rm", file.getAbsolutePath());
+                svnCommitMessage.append("Deleted: " + file.getName() + "\n");                
+            }
+            if (file.exists()) {
+                file.delete();
+            }            
+        }
+    }
+        
+    public BlogEntrySummary findBlogEntry(String title) throws Exception {
+        return (BlogEntrySummary) findByTitle(title, blog.values());
+    }
+    
+    public BlogEntrySummary findBlogEntryByURL(String url) throws Exception {
+        return (BlogEntrySummary) findByURL(url, blog.values());
+    }
+    
+    private static AbstractPage findByURL(String url, Collection<? extends AbstractPage> pages) throws Exception {
+        for (AbstractPage p : pages) {
+            if (p.getURL().endsWith(url)) {
+                return p;
+            }
+        }
+        return null;
+    }
+    
+    private static AbstractPage findByTitle(String title, Collection<? extends AbstractPage> pages) throws Exception {
+        for (AbstractPage p : pages) {
+            if (title.equals(p.getTitle())) {
+                return p;
+            }
+        }
+        return null;
+    }
+    
+    public void loadPages() throws Exception {
+        Document doc = DOMUtils.newDocument();
+        Element el = doc.createElementNS(SOAPNS, "ns1:getPages");
+        Element el2 = doc.createElement("in0");
+        el.appendChild(el2);
+        el2.setTextContent(loginToken);
+        el2 = doc.createElement("in1");
+        el.appendChild(el2);
+        el2.setTextContent(spaceKey);
+        doc.appendChild(el);
+        doc = getDispatch().invoke(doc);
+        
+        Set<String> allPages = new CopyOnWriteArraySet<String>(pages.keySet());
+        Set<Page> newPages = new CopyOnWriteArraySet<Page>();
+        List<Future<?>> futures = new ArrayList<Future<?>>(allPages.size());
+        
+        // XMLUtils.printDOM(doc.getDocumentElement());
+
+        Node nd = doc.getDocumentElement().getFirstChild().getFirstChild();
+        while (nd != null) {
+            if (nd instanceof Element) {
+                futures.add(loadPage((Element)nd, allPages, newPages));
+            }
+            nd = nd.getNextSibling();
+        }
+        for (Future<?> f : futures) {
+            //wait for all the pages to be done
+            f.get();
+        }
+        for (Page p : newPages) {
+            //pages have been added, need to check
+            checkForChildren(p);
+        }
+        for (String id : allPages) {
+            //these pages have been deleted
+            Page p = pages.remove(id);
+            checkForChildren(p);
+            
+            File file = new File(outputDir, p.createFileName());
+            if (file.exists()) {
+                callSvn("rm", file.getAbsolutePath());
+                svnCommitMessage.append("Deleted: " + file.getName() + "\n");                
+            }
+            if (file.exists()) {
+                file.delete();
+            }            
+        }
+        while (checkIncludes()) {
+            // nothing
+        }
+        
+    }
+
+    public boolean checkIncludes() {
+        for (Page p : modifiedPages) {
+            if (checkIncludes(p)) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    public boolean checkIncludes(Page p) {
+        for (Page p2 : pages.values()) {
+            if (p2.includesPage(p.getTitle())
+                && !modifiedPages.contains(p2)) {
+                modifiedPages.add(p2);
+                return true;
+            }
+        }
+        return false;
+    }
+    public void checkForChildren(Page p) {
+        Page parent = pages.get(p.getParentId());
+        int d = 1;
+        while (parent != null) {
+            for (Page p2 : pages.values()) {
+                if (p2.hasChildrenOf(parent.getTitle(), d)
+                    && !modifiedPages.contains(p2)) {
+                    modifiedPages.add(p2);
+                }
+            }
+            parent = pages.get(parent.getParentId());
+            d++;
+        }
+    }
+    
+    public static synchronized Space getSpace(String key) { 
+        Space space = spaces.get(key);
+        if (space == null) {
+            try {
+                doLogin();
+                
+                Document doc = DOMUtils.newDocument();
+                Element el = doc.createElementNS(SOAPNS, "ns1:getSpace");
+                Element el2 = doc.createElement("in0");
+                el.appendChild(el2);
+                el2.setTextContent(loginToken);
+                el2 = doc.createElement("in1");
+                el.appendChild(el2);
+                el2.setTextContent(key);
+                doc.appendChild(el);
+                
+                Document out = getDispatch().invoke(doc);
+                space = new Space(out);
+                spaces.put(key, space);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return space;
+    }
+    
+    public Future<?> loadPage(Element pageSumEl,
+                         final Set<String> allPages,
+                         final Set<Page> newPages) throws Exception {
+        Document doc = DOMUtils.newDocument();
+        Element el = doc.createElementNS(SOAPNS, "ns1:getPage");
+        Element el2 = doc.createElement("in0");
+        el.appendChild(el2);
+        el2.setTextContent(loginToken);
+        el2 = doc.createElement("in1");
+        el.appendChild(el2);
+        el2.setTextContent(DOMUtils.getChildContent(pageSumEl, "id"));
+        doc.appendChild(el);
+        
+        //make sure we only fire off about 15-20 or confluence may get a bit overloaded
+        while (asyncCount.get() > 15) {
+            Thread.sleep(10);
+        }
+        asyncCount.incrementAndGet();
+        Future<?> f = getDispatch().invokeAsync(doc, new AsyncHandler<Document>() {
+            public void handleResponse(Response<Document> doc) {
+                try {
+                    Page page = new Page(doc.get(), SiteExporter.this);
+                    page.setExporter(SiteExporter.this);
+                    Page oldPage = pages.put(page.getId(), page);
+                    if (oldPage == null || page.getModifiedTime().compare(oldPage.getModifiedTime()) > 0) {
+                        if (!modifiedPages.contains(page)) {
+                            modifiedPages.add(page);
+                        }
+                        if (oldPage == null) {
+                            //need to check parents to see if it has a {children} tag so we can re-render
+                            newPages.add(page);
+                        }
+                    }
+                    if (allPages.contains(page.getId())) {
+                        allPages.remove(page.getId());
+                    }
+                } catch (Exception e) {
+                    e.printStackTrace();
+                } finally {
+                    asyncCount.decrementAndGet();
+                }
+            }
+        });
+        return f;
+    }    
+    
+    private String updateContentLinks(AbstractPage page, String content,
+                                      String id, String divCls) throws Exception {
+        XMLReader parser = createTagSoupParser();
+        StringWriter w = new StringWriter();
+        parser.setContentHandler(createContentHandler(page, w, id, divCls));
+        parser.parse(new InputSource(new StringReader(content)));
+        content = w.toString();
+        
+        if (content.indexOf("html>") != -1) {
+            content = content.substring("<html><body>".length());
+            content = content.substring(0, content.lastIndexOf("</body></html>"));
+        }
+        
+        int idx = content.indexOf('>');
+        if (idx != -1
+            && content.substring(idx + 1).startsWith("<p></p>")) {
+            //new confluence tends to stick an empty paragraph at the beginning for some pages (like Banner)
+            //that causes major formatting issues.  Strip it.
+            content = content.substring(0, idx + 1) + content.substring(idx + 8);
+        }
+        return content;
+    }
+    protected XMLReader createTagSoupParser() throws Exception {
+        XMLReader reader = new Parser();
+        reader.setFeature(Parser.namespacesFeature, false);
+        reader.setFeature(Parser.namespacePrefixesFeature, false);
+        reader.setProperty(Parser.schemaProperty, new org.ccil.cowan.tagsoup.HTMLSchema() {
+            {
+                //problem with nested lists that the confluence {toc} macro creates
+                elementType("ul", M_LI, M_BLOCK | M_LI, 0);
+            }
+        });
+
+        return reader;
+    }
+    protected ContentHandler createContentHandler(AbstractPage page, Writer w, 
+                                                  String id, String divCls) {
+        XMLWriter xmlWriter = new ConfluenceCleanupWriter(this, w, page, id, divCls);
+        xmlWriter.setOutputProperty(XMLWriter.OMIT_XML_DECLARATION, "yes");
+        xmlWriter.setOutputProperty(XMLWriter.METHOD, "html");
+        return xmlWriter;
+    }
+
+    public static void main(String[] args) throws Exception {
+        Authenticator.setDefault(new Authenticator() {
+            protected PasswordAuthentication getPasswordAuthentication() {
+                return new PasswordAuthentication(userName, password.toCharArray());
+            }            
+        });
+        ListIterator<String> it = Arrays.asList(args).listIterator();
+        List<String> files = new ArrayList<String>();
+        boolean forceAll = false;
+        int maxThreads = -1;
+        while (it.hasNext()) {
+            String s = it.next();
+            if ("-debug".equals(s)) {
+                debug = true;
+            } else if ("-user".equals(s)) {
+                userName = it.next(); 
+            } else if ("-password".equals(s)) {
+                password = it.next(); 
+            } else if ("-d".equals(s)) {
+                rootOutputDir = new File(it.next());
+            } else if ("-force".equals(s)) {
+                forceAll = true;
+            } else if ("-svn".equals(s)) {
+                svn = true;
+            } else if ("-commit".equals(s)) {
+                commit = true;
+            } else if ("-maxThreads".equals(s)) {
+                maxThreads = Integer.parseInt(it.next());
+            } else if (s != null && s.length() > 0) {
+                files.add(s);
+            }
+        }
+        
+        
+        List<SiteExporter> exporters = new ArrayList<SiteExporter>();
+        for (String file : files) {
+            exporters.add(new SiteExporter(file, forceAll));
+        }
+        List<SiteExporter> modified = new ArrayList<SiteExporter>();
+        for (SiteExporter exporter : exporters) {
+            if (exporter.initialize()) {
+                modified.add(exporter);
+            }
+        }
+        
+        // render stuff only if needed
+        if (!modified.isEmpty()) {
+            setSiteExporters(exporters);
+
+            if (maxThreads <= 0) {
+                maxThreads = modified.size();
+            }
+
+            ExecutorService executor = Executors.newFixedThreadPool(maxThreads, new ThreadFactory() {
+                    public Thread newThread(Runnable r) {
+                        Thread t = new Thread(r);
+                        t.setDaemon(true);
+                        return t;
+                    }
+                });
+            List<Future<?>> futures = new ArrayList<Future<?>>(modified.size());
+            for (SiteExporter exporter : modified) {
+                futures.add(executor.submit(exporter));
+            }
+            for (Future<?> t : futures) {
+                t.get();
+            }
+        }
+                
+        if (commit) {
+            File file = FileUtils.createTempFile("svncommit", "txt");
+            FileWriter writer = new FileWriter(file);
+            writer.write(svnCommitMessage.toString());
+            writer.close();
+            callSvn(rootOutputDir, "commit", "-F", file.getAbsolutePath(), rootOutputDir.getAbsolutePath());
+            svnCommitMessage.setLength(0);
+        }
+    }
+
+    public boolean hasChildren(Page page) {
+        for (Page p : pages.values()) {
+            if (p == page) {
+                continue;
+            }
+            if (page.getId().equals(p.getParentId())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public List<Page> getChildren(Page page) {
+        List<Page> children = new ArrayList<Page>();
+        for (Page p : pages.values()) {
+            if (p == page) {
+                continue;
+            }
+            if (page.getId().equals(p.getParentId())) {
+                children.add(p);
+            }
+        }
+        return children;
+    }
+
+    public String link(Page page) {
+        return page.getLink();
+    }
+
+    public Space getSpace() {
+        if (space == null) {
+            space = getSpace(spaceKey);
+        }
+        return space;
+    }
+    
+    private static void setSiteExporters(List<SiteExporter> exporters) {
+        siteExporters = exporters;
+    }
+
+    public int getAPIVersion() {
+        return apiVersion;
+    }
+
+    public String stripHost(String value) {
+        if (value.startsWith(HOST)) {
+            value = value.substring(HOST.length());
+        }
+        return value;
+    }
+    
+}
diff --git a/src/main/java/org/apache/cxf/cwiki/Space.java b/src/main/java/org/apache/cxf/cwiki/Space.java
new file mode 100644
index 0000000..d65229b
--- /dev/null
+++ b/src/main/java/org/apache/cxf/cwiki/Space.java
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.cwiki;
+
+import org.w3c.dom.Document;
+
+import org.apache.cxf.helpers.DOMUtils;
+
+/**
+ * 
+ */
+public class Space {
+
+    final String key;
+    final String name;
+    final String url;
+    
+    public Space(Document doc) throws Exception {
+        // org.apache.cxf.helpers.XMLUtils.printDOM(doc.getDocumentElement());
+
+        key = DOMUtils.getChildContent(doc.getDocumentElement().getFirstChild(), "key");
+        name = DOMUtils.getChildContent(doc.getDocumentElement().getFirstChild(), "name");
+        url = DOMUtils.getChildContent(doc.getDocumentElement().getFirstChild(), "url");
+    }
+    
+    public String getKey() {
+        return key;
+    }
+    public String getName() {
+        return name;
+    }
+    public String getURL() {
+        return url;
+    }
+
+}
diff --git a/src/main/resources/confluence-rest.wadl b/src/main/resources/confluence-rest.wadl
new file mode 100644
index 0000000..0fd83bf
--- /dev/null
+++ b/src/main/resources/confluence-rest.wadl
@@ -0,0 +1,451 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<application xmlns="http://wadl.dev.java.net/2009/02">
+    <resources base="https://cwiki.apache.org/confluence/rest/api/latest/">
+        <resource path="/content">
+            <method name="POST" id="createContent">
+                <request>
+                    <representation mediaType="application/json"/>
+                </request>
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+            <method name="GET" id="getContent">
+                <request>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="page" type="xs:string" style="query" name="type"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="spaceKey"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="title"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="status"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="postingDay"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="expand"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="query" name="start"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="25" type="xs:int" style="query" name="limit"/>
+                </request>
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+            <resource path="/{id}">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+                <method name="GET" id="getContentById">
+                    <request>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="status"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="query" name="version"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="history,space,version" type="xs:string" style="query" name="expand"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+                <method name="PUT" id="update">
+                    <request>
+                        <representation mediaType="application/json"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+                <method name="DELETE" id="delete">
+                    <request>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="status"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+            <resource path="/{id}/history/{version}/macro/hash/{hash}">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="template" name="version"/>
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="hash"/>
+                <method name="GET" id="getMacroBodyByHash">
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+            <resource path="/{id}/history/{version}/macro/id/{macroId}">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="macroId"/>
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="template" name="version"/>
+                <method name="GET" id="getMacroBodyByMacroId">
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+            <resource path="/{id}/history">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+                <method name="GET" id="getHistory">
+                    <request>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="previousVersion,nextVersion,lastUpdated" type="xs:string" style="query" name="expand"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+            <resource path="/search">
+                <method name="GET" id="search">
+                    <request>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="cql"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="cqlcontext"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="expand"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="query" name="start"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="25" type="xs:int" style="query" name="limit"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+        </resource>
+        <resource path="/content/{id}/property">
+            <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+            <method name="GET" id="findAll">
+                <request>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="expand"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="query" name="start"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="10" type="xs:int" style="query" name="limit"/>
+                </request>
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+            <method name="POST" id="create">
+                <request>
+                    <representation mediaType="application/json"/>
+                </request>
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+            <resource path="{key}">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="key"/>
+                <method name="GET" id="findByKey">
+                    <request>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="expand"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+                <method name="PUT" id="update">
+                    <request>
+                        <representation mediaType="application/json"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+                <method name="DELETE" id="delete">
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+                <method name="POST" id="create">
+                    <request>
+                        <representation mediaType="application/json"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+        </resource>
+        <resource path="/content/{id}/restriction">
+            <resource path="byOperation/{operationKey}">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="operationKey"/>
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+                <method name="GET" id="forOperation">
+                    <request>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="restrictions.user,restrictions.group" type="xs:string" style="query" name="expand"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="query" name="start"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="100" type="xs:int" style="query" name="limit"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+            <resource path="byOperation">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+                <method name="GET" id="byOperation">
+                    <request>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="update.restrictions.user,read.restrictions.group,read.restrictions.user,update.restrictions.group" type="xs:string" style="query" name="expand"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+        </resource>
+        <resource path="/contentbody/convert/{to}">
+            <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="to"/>
+            <method name="POST" id="convert">
+                <request>
+                    <representation mediaType="application/json"/>
+                </request>
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+        </resource>
+        <resource path="/content/{id}/child">
+            <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+            <method name="GET" id="children">
+                <request>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="expand"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="0" type="xs:int" style="query" name="parentVersion"/>
+                </request>
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+            <resource path="/{type}">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="type"/>
+                <method name="GET" id="childrenOfType">
+                    <request>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="expand"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="0" type="xs:int" style="query" name="parentVersion"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="query" name="start"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="25" type="xs:int" style="query" name="limit"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+            <resource path="/comment">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+                <method name="GET" id="commentsOfContent">
+                    <request>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="expand"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="0" type="xs:int" style="query" name="parentVersion"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="query" name="start"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="25" type="xs:int" style="query" name="limit"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="location"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="depth"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+        </resource>
+        <resource path="/content/{id}/child/attachment">
+            <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+            <method name="POST" id="createAttachments">
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+            <method name="GET" id="getAttachments">
+                <request>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="expand"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="query" name="start"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="50" type="xs:int" style="query" name="limit"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="filename"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="mediaType"/>
+                </request>
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+            <resource path="/{attachmentId}/data">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="attachmentId"/>
+                <method name="POST" id="updateData">
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+            <resource path="/{attachmentId}">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="attachmentId"/>
+                <method name="PUT" id="update">
+                    <request>
+                        <representation mediaType="application/json"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+        </resource>
+        <resource path="/longtask">
+            <method name="GET" id="getTasks">
+                <request>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="expand"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="query" name="start"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="100" type="xs:int" style="query" name="limit"/>
+                </request>
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+            <resource path="/{id}">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+                <method name="GET" id="getTask">
+                    <request>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="expand"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+        </resource>
+        <resource path="/content/{id}/label">
+            <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+            <method name="DELETE" id="deleteLabelWithQueryParam">
+                <request>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="name"/>
+                </request>
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+            <method name="POST" id="addLabels">
+                <request>
+                    <representation mediaType="application/json"/>
+                </request>
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+            <method name="GET" id="labels">
+                <request>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="prefix"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="query" name="start"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="200" type="xs:int" style="query" name="limit"/>
+                </request>
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+            <resource path="/{label}">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="label"/>
+                <method name="DELETE" id="deleteLabel">
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+        </resource>
+        <resource path="/content/{id}/descendant">
+            <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+            <method name="GET" id="descendants">
+                <request>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="expand"/>
+                </request>
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+            <resource path="/{type}">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="id"/>
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="type"/>
+                <method name="GET" id="descendantsOfType">
+                    <request>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="expand"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="query" name="start"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="25" type="xs:int" style="query" name="limit"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+        </resource>
+        <resource path="/space">
+            <method name="GET" id="spaces">
+                <request>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="query" name="spaceKey"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="expand"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="query" name="start"/>
+                    <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="25" type="xs:int" style="query" name="limit"/>
+                </request>
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+            <method name="POST" id="createSpace">
+                <request>
+                    <representation mediaType="application/json"/>
+                </request>
+                <response>
+                    <representation mediaType="application/json"/>
+                </response>
+            </method>
+            <resource path="{spaceKey}/content/{type}">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="spaceKey"/>
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="type"/>
+                <method name="GET" id="contentsWithType">
+                    <request>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="all" type="xs:string" style="query" name="depth"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="expand"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="query" name="start"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="25" type="xs:int" style="query" name="limit"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+            <resource path="_private">
+                <method name="POST" id="createPrivateSpace">
+                    <request>
+                        <representation mediaType="application/json"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+            <resource path="/{spaceKey}">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="spaceKey"/>
+                <method name="PUT" id="update">
+                    <request>
+                        <representation mediaType="application/json"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+                <method name="DELETE" id="delete">
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+            <resource path="{spaceKey}/content">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="spaceKey"/>
+                <method name="GET" id="contents">
+                    <request>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="all" type="xs:string" style="query" name="depth"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="expand"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:int" style="query" name="start"/>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="25" type="xs:int" style="query" name="limit"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+            <resource path="{spaceKey}">
+                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="spaceKey"/>
+                <method name="GET" id="space">
+                    <request>
+                        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="" type="xs:string" style="query" name="expand"/>
+                    </request>
+                    <response>
+                        <representation mediaType="application/json"/>
+                    </response>
+                </method>
+            </resource>
+        </resource>
+    </resources>
+</application>
diff --git a/src/test/java/org/apache/cxf/cwiki/PageTest.java b/src/test/java/org/apache/cxf/cwiki/PageTest.java
new file mode 100644
index 0000000..735cad4
--- /dev/null
+++ b/src/test/java/org/apache/cxf/cwiki/PageTest.java
@@ -0,0 +1,55 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cxf.cwiki;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.w3c.dom.Document;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+@Test
+public class PageTest {
+
+    @Test
+    public void shouldParseUpperCasedCodeMacro() throws Exception {
+        // given
+        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
+        DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
+        Document doc = docBuilder.parse("src/test/resources/page.xml");
+
+        SiteExporter se = new SiteExporter("src/test/resources/docs.cfg", true);
+        SiteExporter.apiVersion = 1;
+
+        Page p = new Page(doc, se);
+
+        // when
+        Set<String> codeScripts = new HashSet<String>(p.getCodeScripts());
+        Set<String> expected = new HashSet<String>(Arrays.asList("shBrushJava.js", "shBrushXml.js", "shBrushPlain.js"));
+        
+        Assert.assertEquals(codeScripts, expected);
+    }
+
+}
diff --git a/src/test/resources/docs.cfg b/src/test/resources/docs.cfg
new file mode 100644
index 0000000..139597f
--- /dev/null
+++ b/src/test/resources/docs.cfg
@@ -0,0 +1,2 @@
+
+
diff --git a/src/test/resources/page.xml b/src/test/resources/page.xml
new file mode 100644
index 0000000..0b888ad
--- /dev/null
+++ b/src/test/resources/page.xml
@@ -0,0 +1,29 @@
+<page>
+    <body>
+        <modified>2013-07-19T08:27:39</modified>
+        <content>
+            {code:plain|title=Register.properties}
+            thankyou=Thank you for registering %{personBean.firstName}.
+            {code}
+
+            {code:html|title=textfield tag}
+            <s:textfield name="personBean.firstName" label="First name"/>
+            {code}
+
+            {code:html|title=link to register.jsp}
+            <p>
+                <a href="register.jsp">Please register</a>
+                for our prize drawing.
+            </p>
+            {code}
+
+            {code:XML|title=registerInput action node for struts.xml}
+            <action name="registerInput" class="org.apache.struts.register.action.Register" method="input">
+                <result name="input">/register.jsp</result>
+            </action>
+            {code}
+        </content>
+        <attachments size="6"/>
+        <labels/>
+    </body>
+</page>
\ No newline at end of file
diff --git a/template/docs.cfg b/template/docs.cfg
new file mode 100644
index 0000000..9152cb6
--- /dev/null
+++ b/template/docs.cfg
@@ -0,0 +1,6 @@
+spaceKey: CXF20DOC
+pageCacheFile:/cache/docs.pageCache
+templateName:template/template.vm
+outputDir:/docs
+globalPages:Navigation,Banner,QuickLinks
+
diff --git a/template/main.cfg b/template/main.cfg
new file mode 100644
index 0000000..4c9a356
--- /dev/null
+++ b/template/main.cfg
@@ -0,0 +1,6 @@
+spaceKey: CXF
+pageCacheFile:cache/main.pageCache
+templateName:template/template.vm
+outputDir:/
+globalPages:Navigation,Banner,QuickLinks
+
diff --git a/template/template.vm b/template/template.vm
new file mode 100644
index 0000000..fb614f4
--- /dev/null
+++ b/template/template.vm
@@ -0,0 +1,162 @@
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<html>
+  <head>
+
+<link type="text/css" rel="stylesheet" href="/resources/site.css">
+<script src='/resources/space.js'></script>
+
+<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+<meta name="keywords" content="business integration, EAI, SOA, Service Oriented Architecture, web services, SOAP, JBI, JMS, WSDL, XML, EDI, Electronic Data Interchange, standards support, integration standards, application integration, middleware, software, solutions, services, CXF, open source">
+<meta name="description" content="Apache CXF, Services Framework - $page.title">
+
+
+#if($page.hasCode)
+<link type="text/css" rel="stylesheet" href="/resources/highlighter/styles/shCoreCXF.css">
+<link type="text/css" rel="stylesheet" href="/resources/highlighter/styles/shThemeCXF.css">
+
+<script src='/resources/highlighter/scripts/shCore.js'></script>
+#foreach ($hscript in $page.CodeScripts)
+<script src='/resources/highlighter/scripts/$hscript'></script>
+#end
+<script>
+  SyntaxHighlighter.defaults['toolbar'] = false;
+  SyntaxHighlighter.all();
+</script>
+#end
+
+
+    <title>
+Apache CXF -- $page.title
+    </title>
+  </head>
+<body onload="init()">
+
+
+<table width="100%" cellpadding="0" cellspacing="0">
+  <tr>
+    <td id="cell-0-0" colspan="2">&nbsp;</td>
+    <td id="cell-0-1">&nbsp;</td>
+    <td id="cell-0-2" colspan="2">&nbsp;</td>
+  </tr>
+  <tr>
+    <td id="cell-1-0">&nbsp;</td>
+    <td id="cell-1-1">&nbsp;</td>
+    <td id="cell-1-2">
+      <!-- Banner -->
+#set($content = $exporter.getPageContent("Banner", "banner", "banner"))
+$content
+      <!-- Banner -->
+      <div id="top-menu">
+        <table border="0" cellpadding="1" cellspacing="0" width="100%">
+          <tr>
+            <td>
+              <div align="left">
+                <!-- Breadcrumbs -->
+#set($content = $exporter.breadcrumbs($page))
+$content
+                <!-- Breadcrumbs -->
+              </div>
+            </td>
+            <td>
+              <div align="right">
+                <!-- Quicklinks -->
+#set($content = $exporter.getPageContent("QuickLinks", "quicklinks"))
+$content
+                <!-- Quicklinks -->
+              </div>
+            </td>
+          </tr>
+        </table>
+      </div>
+    </td>
+    <td id="cell-1-3">&nbsp;</td>
+    <td id="cell-1-4">&nbsp;</td>
+  </tr>
+  <tr>
+    <td id="cell-2-0" colspan="2">&nbsp;</td>
+    <td id="cell-2-1">
+      <table>
+        <tr valign="top">
+          <td height="100%">
+            <div id="wrapper-menu-page-right">
+              <div id="wrapper-menu-page-top">
+                <div id="wrapper-menu-page-bottom">
+                  <div id="menu-page">
+                    <!-- NavigationBar -->
+#set($content = $exporter.getPageContent("Navigation", "navigation"))
+$content
+                    <!-- NavigationBar -->
+                  </div>
+              </div>
+            </div>
+          </div>
+         </td>
+         <td height="100%">
+           <!-- Content -->
+           <div class="wiki-content">
+$body
+           </div>
+           <!-- Content -->
+         </td>
+        </tr>
+      </table>
+   </td>
+   <td id="cell-2-2" colspan="2">&nbsp;</td>
+  </tr>
+  <tr>
+   <td id="cell-3-0">&nbsp;</td>
+   <td id="cell-3-1">&nbsp;</td>
+   <td id="cell-3-2">
+     <div id="footer">
+       <!-- Footer -->
+       <div id="site-footer">
+         <a href="http://cxf.apache.org/privacy-policy.html">Privacy Policy</a> - 
+         (<a href="$confluenceUri/pages/editpage.action?pageId=$page.id">edit page</a>) 
+	 (<a href="$confluenceUri/pages/viewpage.action?pageId=$page.id&amp;showComments=true&amp;showCommentArea=true#addcomment">add comment</a>)<br>
+	Apache CXF, CXF, Apache, the Apache feather logo are trademarks of The Apache Software Foundation.<br>
+        All other marks mentioned may be trademarks or registered trademarks of their respective owners.
+       </div>
+       <!-- Footer -->
+     </div>
+   </td>
+   <td id="cell-3-3">&nbsp;</td>
+   <td id="cell-3-4">&nbsp;</td>
+  </tr>
+  <tr>
+    <td id="cell-4-0" colspan="2">&nbsp;</td>
+    <td id="cell-4-1">&nbsp;</td>
+    <td id="cell-4-2" colspan="2">&nbsp;</td>
+  </tr>
+</table>
+
+<script type="text/javascript">
+var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
+document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
+</script>
+<script type="text/javascript">
+try {
+var pageTracker = _gat._getTracker("UA-4458903-1");
+pageTracker._trackPageview();
+} catch(err) {}</script>
+
+</body>
+</html>
+
diff --git a/update-site b/update-site
new file mode 100755
index 0000000..e196015
--- /dev/null
+++ b/update-site
@@ -0,0 +1,4 @@
+#!/bin/sh
+mvn -Pconfluence,nochecks -e package exec:java -Dsite.output=content $@
+[ $? -eq 0 ] || exit -1
+