| 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; |
| } |
| |
| } |
| |
| |
| } |