blob: b6c4def2c72a4144c5c270ac04556f0b33ceab17 [file] [log] [blame]
/**
* 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 = " > ";
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;
}
}