blob: d6c0058c861687078660d881199a705a82b66824 [file] [log] [blame]
package com.atlassian.uwc.hierarchies;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringEscapeUtils;
import com.atlassian.uwc.ui.Page;
public class SmfHierarchy extends MetaHierarchy {
private static final String PROPKEY_COMMENTS = "reply-comments";
private static final String PROPKEY_HIERARCHYCOMPARATOR = "hierarchy-child-comparator";
private static final String TITLE_DELIM = " - ";
private static final String DEFAULT_DATEFORMAT = "yyyy-MM-dd hmma";
private static final String DEFAULT_EMPTY_TIME = "1";
private static final String DEFAULT_EMPTY_NAME = "No Name";
private static final String DEFAULT_EMPTY_TITLE = "No Title";
public static final String DEFAULT_ROOTPAGENAME = "Home";
@Override
public HierarchyNode buildHierarchy(Collection<Page> pages) {
HierarchyNode root = super.buildHierarchy(pages);
handleComments(root);
return root;
}
@Override
protected HierarchyNode buildRelationships(Page page, HierarchyNode root) {
assignRootPage(root);
log.debug("page: " + page.getName());
try {
Properties meta = getMeta(page);
String ancestorString = meta.getProperty("ancestors", "");
if ("null".equals(ancestorString)) ancestorString = null;
HierarchyNode node = root.getChildren().iterator().next(); //first child = Home. root will be ignored by engine.
if (ancestorString != null && !"".equals(ancestorString)) { //set up ancestors
String[] ancestors = ancestorString.split(":");
for (int i = 0; i < ancestors.length; i++) {
String ancestor = ancestors[i];
node = buildNode(node, ancestor);
}
}
node = buildNode(node, createNodeId(meta.getProperty("id"), meta.getProperty("type")), page);
convertPagename(page, meta);
node.setName(page.getName());
} catch (FileNotFoundException e) {
String error = "Meta file for page does not exist: " + getMetaPath(page);
log.warn(error);
} catch (IOException e) {
String error = "Problem opening meta page: " + getMetaPath(page);
log.warn(error);
e.printStackTrace();
}
return root;
}
protected void init() { //called from MetaHierarchy.buildHierarchy
super.init();
clearCollisions();
}
/**
* contains already used titles, so we can detect collisions
*/
HashMap<String,String> collisions = null;
/**
* updates the page title with the info from the meta properties.
* If the page title is not a valid Confluence title, it removes
* any disallowed characters. It also checks for namespace collisions
* and updates the page title with the username and timestamp if there
* is one. The timestamp is controlled with the converter property "title-date-format".
* @param page
* @param meta
* @return
*/
protected String convertPagename(Page page, Properties meta) {
String origTitle = meta.getProperty("title", DEFAULT_EMPTY_TITLE);
String title = removeIllegalTitleChars(origTitle);
if ("".equals(title)) title = DEFAULT_EMPTY_TITLE;
//namespace collisions
if (getCollisions().containsKey(title)) {
String origName = meta.getProperty("username", DEFAULT_EMPTY_NAME);
String name = removeIllegalTitleChars(origName);
String type = meta.getProperty("type", "top");
if (type.equals("top") || type.equals("re")) {
String time = meta.getProperty("time", DEFAULT_EMPTY_TIME);
String formattedTime = removeIllegalTitleChars(formatTime(time));
if (!"".equals(formattedTime)) formattedTime = TITLE_DELIM + formattedTime;
title += TITLE_DELIM + name + formattedTime;
}
//unlikely, but possible
int index = 2;
String current = title;
while (getCollisions().containsKey(title)) {
title = current + TITLE_DELIM + "No." + index++;
}
}
getCollisions().put(title, "");
//set the page name
page.setName(title);
return page.getName();
}
/**
* @param time seconds since the epoch for the timestamp we are trying to format
* @return title friendly formatted time (using either the converter property title-date-format
* or the DEFAULT_DATEFORMAT. Date format should be formatted as described in SimpleDateFormat
* @see SimpleDateFormat
*/
protected String formatTime(String time) {
//get format from converter properties
String format = this.properties.getProperty("title-date-format", DEFAULT_DATEFORMAT);
//format time
long seconds;
try {
seconds = Long.parseLong(time);
} catch (Exception e) {
return ""; //can't parse time
}
long milli = seconds * 1000;
Date date = new Date(milli);
DateFormat dateFormat = null;
try {
dateFormat = new SimpleDateFormat(format);
} catch (IllegalArgumentException e) {
log.error("Custom date format is not a valid SimpleDateFormat: " + format
+ ". Using default format instead: " + DEFAULT_DATEFORMAT);
dateFormat = new SimpleDateFormat(DEFAULT_DATEFORMAT);
}
return (dateFormat.format(date));
}
protected static String removeIllegalTitleChars(String input) {
//html entities
input = StringEscapeUtils.unescapeHtml(input);
//illegal characters
input = input.replaceAll("^[$~]", ""); //starting at title with ~ or $ is illegal
input = input.replaceAll("^\\.\\.", "");//starting a title with .. is illegal
//conf illegal chars
input = input.replaceAll("[:;{}\\[\\]<>()@/\\\\|^#]", "");
input = input.trim();
return input;
}
private HashMap<String,String> getCollisions() {
if (this.collisions == null) this.collisions = new HashMap<String, String>();
return this.collisions;
}
public void clearCollisions() {
this.collisions = null;
}
protected HierarchyNode buildNode(HierarchyNode parent, String nodeid) {
return buildNode(parent, nodeid, null);
}
protected HierarchyNode buildNode(HierarchyNode parent, String nodeid, Page page) {
//does parent have this node already?
if (!parent.getChildren().isEmpty()) {
for (Iterator iter = parent.getChildIterator(); iter.hasNext();) {
HierarchyNode child = (HierarchyNode) iter.next();
if (same(nodeid, child)) {
if (child.getPage() == null && page != null) child.setPage(page);
return child;
}
}
}
//if not add it here
HierarchyNode child = new HierarchyNode();
if (page != null) child.setPage(page);
child.setName(nodeid);
parent.addChild(child);
return child;
}
private boolean same(String nodeid, HierarchyNode child) {
if (child == null) return false;
if (child.getName().equals(nodeid)) return true;
if (child.getPage() == null) return false;
return child.getPage().getFile().getName().endsWith(nodeid+".txt");
}
protected String createNodeId(String id, String type) {
return type+id;
}
private void assignRootPage(HierarchyNode root) {
HierarchyNode home;
if (root.getChildren().isEmpty()) {
home = new HierarchyNode();
home.setChildrenComparator(new SmfTimeComparator());
home.setName(DEFAULT_ROOTPAGENAME);
root.addChild(home);
}
}
private void handleComments(HierarchyNode root) {
if (this.getProperties().containsKey(PROPKEY_COMMENTS) &&
Boolean.parseBoolean(this.getProperties().getProperty(PROPKEY_COMMENTS))) {
Vector<HierarchyNode> topicNodes = getTopicNodes(root);
for (HierarchyNode topic : topicNodes) {
Collection<HierarchyNode> children = topic.getChildren();
for (Iterator iter = children.iterator(); iter.hasNext();) {
HierarchyNode reply = (HierarchyNode) iter.next();
String comment = createComment(reply.getName());
if (topic.getPage() == null) continue;// could happen if we're converting only one file
topic.getPage().addComment(comment);
}
}
}
}
Pattern topicPattern = Pattern.compile("_top\\d+\\.txt$");
/**
* recursive method for getting all the topic nodes from within
* a given root. Note that because of child boards, we can't assume
* that topics are all on the 3rd level
* @param root
* @return
*/
protected Vector<HierarchyNode> getTopicNodes(HierarchyNode root) {
Vector<HierarchyNode> topics = new Vector<HierarchyNode>();
String filename;
//use the filename to determine the page's level
if (root.getPage() == null ||
root.getPage().getFile() == null ||
root.getPage().getFile().getName() == null) {
filename = "";
}
else filename = root.getPage().getFile().getName();
//if it's a topic, add it
if (topicPattern.matcher(filename).find())
topics.add(root);
else { //check it's children for topic pages
for (HierarchyNode node : root.getChildren()) {
topics.addAll(getTopicNodes(node));
}
}
return topics;
}
protected String createComment(String name) {
return "h1. [" + name + "]\n{include:" + name + "}";
}
/**
* Pattern for identifying object id from filename
*/
Pattern id = Pattern.compile("(\\d+)(\\.txt)?$");
/**
* Uses the id of the object to determine it's sort order.
* (Smaller ids mean the object was created earlier.)
*/
public class SmfTimeComparator implements Comparator {
public int compare(Object arg0, Object arg1) {
HierarchyNode node0 = (HierarchyNode) arg0;
HierarchyNode node1 = (HierarchyNode) arg1;
String name0 = node0.getName();
String name1 = node1.getName();
if (!id.matcher(name0).find() && node0.getPage() != null)
name0 = node0.getPage().getFile().getName();
if (!id.matcher(name1).find() && node1.getPage() != null)
name1 = node1.getPage().getFile().getName();
Matcher idFinder0 = id.matcher(name0);
Matcher idFinder1 = id.matcher(name1);
if (idFinder0.find() && idFinder1.find()) {
String idStr0 = idFinder0.group(1);
String idStr1 = idFinder1.group(1);
int id0 = Integer.parseInt(idStr0);
int id1 = Integer.parseInt(idStr1);
return id0 - id1;
}
return 0;
}
}
}