blob: ac7c1d6bc3d8de0989f0f60946b83a236e1807f7 [file] [log] [blame]
package com.atlassian.uwc.ui;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.activation.MimetypesFileTypeMap;
import org.apache.log4j.Logger;
import org.apache.xmlrpc.XmlRpcException;
import biz.artemis.confluence.xmlrpcwrapper.AttachmentForXmlRpc;
import biz.artemis.confluence.xmlrpcwrapper.BlogForXmlRpc;
import biz.artemis.confluence.xmlrpcwrapper.CommentForXmlRpc;
import biz.artemis.confluence.xmlrpcwrapper.ConfluenceServerSettings;
import biz.artemis.confluence.xmlrpcwrapper.PageForXmlRpc;
import biz.artemis.confluence.xmlrpcwrapper.RemoteWikiBroker;
import biz.artemis.confluence.xmlrpcwrapper.SpaceForXmlRpc;
import biz.artemis.confluence.xmlrpcwrapper.SpaceForXmlRpc.SpaceType;
import com.atlassian.uwc.converters.Converter;
import com.atlassian.uwc.converters.IllegalLinkNameConverter;
import com.atlassian.uwc.converters.IllegalPageNameConverter;
import com.atlassian.uwc.converters.JavaRegexConverter;
import com.atlassian.uwc.converters.PerlConverter;
import com.atlassian.uwc.converters.RequiresEngineConverter;
import com.atlassian.uwc.converters.twiki.JavaRegexAndTokenizerConverter;
import com.atlassian.uwc.converters.twiki.TWikiRegexConverterCleanerWrapper;
import com.atlassian.uwc.converters.xml.DefaultXmlEvents;
import com.atlassian.uwc.converters.xml.XmlEvents;
import com.atlassian.uwc.filters.FilterChain;
import com.atlassian.uwc.hierarchies.DokuwikiHierarchyTest;
import com.atlassian.uwc.hierarchies.HierarchyBuilder;
import com.atlassian.uwc.hierarchies.HierarchyNode;
import com.atlassian.uwc.splitters.PageSplitter;
import com.atlassian.uwc.ui.listeners.FeedbackHandler;
import com.atlassian.uwc.ui.listeners.TestSettingsListener;
/**
* This class drives the conversion process by gathering all the files, gathering
* the selected converters, then applying all the converters against each file, then
* sending the converted 'Pages' and attachments to Confluence via XML-RPC (or
* possibly some other method in the future)
*/
public class ConverterEngine implements FeedbackHandler {
/* START CONSTANTS */
private static final int NUM_REQ_CONVERTERS = 2;
private static final String REQUIRED_CONVERTER_ILLEGAL_LINKS = "MyWiki.9999.illegal-links.class=com.atlassian.uwc.converters.IllegalLinkNameConverter";
private static final String REQUIRED_CONVERTER_ILLEGAL_NAMES = "MyWiki.9999.illegal-names.class=com.atlassian.uwc.converters.IllegalPageNameConverter";
private static final String NONCONVERTERTYPE_PAGEHISTORYPRESERVATION = "page-history-preservation";
private static final String NONCONVERTERTYPE_HIERARCHYBUILDER = ".hierarchy-builder";
private static final String NONCONVERTERTYPE_ILLEGALHANDLING = "illegal-handling";
private static final String NONCONVERTERTYPE_AUTODETECTSPACEKEYS = "autodetect-spacekeys";
private static final String NONCONVERTERTYPE_MISCPROPERTIES = ".property";
private static final String NONCONVERTERTYPE_FILTERS = ".filter";
private static final String NONCONVERTERTYPE_XMLEVENT = ".xmlevent";
private static final String CONVERTERTYPE_TWIKICLEANER = ".twiki-cleaner";
private static final String CONVERTERTYPE_JAVAREGEX = ".java-regex";
private static final String CONVERTERTYPE_JAVAREGEXTOKEN = ".java-regex-tokenize";
private static final String CONVERTERTYPE_PERL = ".perl";
private static final String CONVERTERTYPE_CLASS = ".class";
private static final String XMLEVENT_PROP_ERROR = "Xmlevent Property must follow this format convention: {tag}xmltag{class}com.something.Class";
private static final String PROP_ATTACHMENT_SIZE_MAX = "attachment.size.max";
private static final int DEFAULT_NUM_STEPS = 1000;
private static final String ORPHAN_ATTACHMENTS_PAGE_TITLE="Orphan attachments";
private static final String DEFAULT_ATTACHMENT_UPLOAD_COMMENT = "Added by UWC, the Universal Wiki Converter";
public static final String PROPKEY_ENGINE_SAVES_TO_DISK = "engine-saves-to-disk";
private static final String PROPKEY_SPACEPERMS = "spaceperms";
/* START FIELDS */
public boolean running = false; //Methods check this to see if the conversion needs to be cancelled
/**
* used to disable check for illegal names and links.
* We want to allow users to override this so they can handle it themselves
* with converters.
*/
private boolean illegalHandlingEnabled = false; //default = false, as of Confluence 4.2 doesn't appear to be necessary
private boolean autoDetectSpacekeys = false; //default = false
private HashSet<String> attachedFiles;//attachmentids
private HashSet<String> attachedPaths;//attachment file paths
Logger log = Logger.getLogger(this.getClass());
// this logger is used to write out totals for the UWC to a seperate file uwc-totals.log
Logger totalsFileLog = Logger.getLogger("totalsFileLog");
Logger attachmentLog = Logger.getLogger("attachmentsLog");
/**
* The string that directory separators (e.g., / on Unix and \ on Windows) are replaced
* with in page titles.
* This is used by DokuWikiLinkConverter too.
*/
public static final String CONFLUENCE_SEPARATOR = " -- ";
protected enum HierarchyHandler {
DEFAULT, //no hierarchy handling
HIERARCHY_BUILDER, //hierarchyBuilder handles
PAGENAME_HIERARCHIES//hierarchy maintained in pagename
}
private HierarchyHandler hierarchyHandler = HierarchyHandler.DEFAULT;
/**
* The mapping from file name extension to mime type that is used when sending
* attachments to Confluence.
* NOTE: static so that other files can get access to this easily.
*/
private static MimetypesFileTypeMap mimeTypes;
/**
* This is the location of the mime type mapping file. For details on the file format,
* refer to the link below.
*
* @see javax.activation.MimetypesFileTypeMap
*/
public final static String mimetypeFileLoc = "conf" + File.separator + "mime.types";
/**
* This field is set if a hierarchy builder "converter" is used. The field controls the
* way in which pages are added/updated in Confluence. If hierarchyBuilder is <code>null</code>, all
* pages are added as top-level pages in the selected space. Otherwise, the hierarchy builder is
* called on to create a page hierarchy, and the engine will insert the pages correspondingly.
*/
private HierarchyBuilder hierarchyBuilder = null;
private UWCUserSettings settings;
private State state;
private Properties miscProperties = new Properties(); //instantiate this here - UWC-293
private Set<String> filterValues;
/**
* the number of properties that are not converters from the properties file.
* When we set up the progress bar, we calculate the max number of steps
* we're going to encounter with the number of properties that could be converters.
* But we update the progress bar a step only foreach converter property. So, we'll use
* this field to update the progress bar the extra amount.
*/
private int numNonConverterProperties;
private Feedback feedback;
private int newNodes;
HashMap<String, Converter> converterCacheMap = new HashMap<String, Converter>();
private long startTotalConvertTime;
//Error handlers
private ConverterErrors errors = new ConverterErrors();
private boolean hadConverterErrors;
private HashMap<String, String> homepages = new HashMap<String, String>();
/* START CONSTRUCTORS */
/**
* This default constructor initializes the mime types.
*/
public ConverterEngine() {
try {
mimeTypes = new MimetypesFileTypeMap(new FileInputStream(mimetypeFileLoc));
} catch (FileNotFoundException e) {
String note = "Couldn't load mime types!";
log.error(note, e);
this.errors.addError(Feedback.BAD_SETTINGS_FILE, note, false);
}
totalsFileLog.setAdditivity(false);
}
/* START METHODS */
/**
* converts the files with the converterstrings, and hooks any feedback into the given ui
* @param inputPages pages from the filesystem to be converted
* @param converterStrings list of converters as strings which will be run on the pages
* @param sendToConfluence true if the pages should be uploaded to confluence
* @param wikitype The wiki type that's being converted into Confluence, ex: Mediawiki
*/
public void convert(List<File> inputPages, List<String> converterStrings, UWCUserSettings settings) {
//setup
this.running = true;
resetFeedback();
resetErrorHandlers();
resetHierarchy();
//settings
boolean sendToConfluence = Boolean.parseBoolean(settings.getSendToConfluence());
this.settings = settings;
if (!this.running) {
this.feedback = Feedback.CANCELLED;
return;
}
//convert
convert(inputPages, converterStrings, sendToConfluence, settings.getPattern());
//cleanup
if (this.feedback == Feedback.NONE)
this.feedback = Feedback.OK;
this.running = false;
}
/**
* cancels the conversion
*/
public void cancel() {
String message = "Engine - Sending Cancel Signal";
log.debug(message);
this.state.updateNote(message);
this.running = false;
}
/**
* gets a new State object.
* The State object will be used by the converter engine to measure progress.
* @param inputPages list of pages
* @param converterStrings list of converters
* @param settings settings
* @return the state object the engine uses
*/
public State getState(List<File> inputPages, List<String> converterStrings, UWCUserSettings settings) {
//The inputPages and converterString objects do not approximate the number of steps well because
//inputPages includes directories which are counted as 1 object in the inputPages list, and
//converterStrings includes non-converter properties. So, for now we'll use a default value to set up
//the state.
// int steps =
// getNumberOfSteps(
// inputPages,
// converterStrings,
// Boolean.parseBoolean(settings.getSendToConfluence())
// );
return getState(settings);
}
/**
* gets a new State object, using a default number of steps.
* The State object will be used by the converter engine to measure progress.
* @param settings
* @return
*/
public State getState(UWCUserSettings settings) {
int steps = DEFAULT_NUM_STEPS;
String initialMessage = "Converting Wiki\n" +
"Wikitype = " + settings.getWikitype() + "\n";
this.state = new State(initialMessage, 0, steps);
return state;
}
/**
* gets the number of steps for the given pages and converters lists
* @param pages list of objects representing pages. Can be any object: String, Page, etc.
* @param converters list of objects representing pages. Can be any object: String, Converter, etc.
* @param sendToConfluence true, if the conversion will upload the objects to confluence
* @return the number of steps that the engine will measure progress against
*/
protected int getNumberOfSteps(List pages, List converters, boolean sendToConfluence) {
return getNumberOfSteps(pages.size(), converters.size(), sendToConfluence);
}
/**
* Counts the number of steps needed to do the entire conversion from start to finish.
* Here are the steps:
* 1. init converters - number of converters
* 2. create page objects - number of pages
* 3. convert the files - num of converterstrings * num of pages (or num of pages)
* 4. convert with required converters - num of pages
* 5. save pages - num of pages
* 6. upload pages (if send to confluence) num of pages, or 0
* @param pages number of pages
* @param converters number of converters
* @param sendToConfluence true if sending pages to confluence
* @return number of steps for performing conversion from start to finish
*/
private int getNumberOfSteps(int pages, int converters, boolean sendToConfluence) {
return getNumberOfSteps(pages, converters, converters, sendToConfluence);
}
/**
* Counts the number of steps needed to do an entire conversion from start to finish.
* Used with progress monitor
* @param pages number of pages
* @param properties number of all converter file properties (including non-converter properties)
* @param converters number of converters
* @param sendToConfluence true if sending pages to Confluence
* @return number of steps for performing conversion from start to finish
*/
private int getNumberOfSteps(int pages, int properties, int converters, boolean sendToConfluence) {
return getNumberOfSteps(pages, pages, properties, converters, sendToConfluence);
}
/**
* Counts the number of steps needed to do an entire conversion from start to finish.
* Used with progress monitor
* @param files number of files or directories chosen by the user. Does not count contents of directories seperately.
* @param pages number of individual pages that will be converted
* @param properties number of all converter file properties (including non-converter properties)
* @param converters number of converters
* @param sendToConfluence true if sending pages to Confluence
* @return number of steps for performing conversion from start to finish
*/
private int getNumberOfSteps(int files, int pages, int properties, int converters, boolean sendToConfluence) {
int numReqConverters = isIllegalHandlingEnabled()?NUM_REQ_CONVERTERS:0;
int steps =
properties + //1. initialize converters (handles both converter and non-converter properties)
files + //2. create page objects (uses the original list of chosen file objects)
(converters * pages) + //3. convert the files (uses the number of page objects)
(numReqConverters) + //4. create required converters (2, right now)
(numReqConverters * pages) + //5. convert with required converters (2, right now)
pages + //6. save the files
(sendToConfluence?pages:0); //7. upload pages if sendToConfluence
return steps;
}
/**
* converts the given pages using the given converterStrings, and sends the pages
* to Confluence, if sendToConfluence is true.
* @param pages
* @param converterStrings
* @param sendToConfluence
*/
public void convert(List<File> pages, List<String> converterStrings, boolean sendToConfluence) {
convert(pages, converterStrings, sendToConfluence, "");
}
/**
* converts the given pages not filtered out with the given filterPattern
* using the given converterStrings, and sends the pages to Confluence,
* if sendToConfluence is true
* @param pages
* @param converterStrings
* @param sendToConfluence
* @param filterPattern ignores files with this filter pattern
*/
public void convert(List<File> pages, List<String> converterStrings, boolean sendToConfluence, String filterPattern) {
log.info("Starting conversion.");
initConversion();
//create converters
ArrayList<Converter> converters = createConverters(converterStrings);
//create page objects - Recurse through directories, adding all files
FileFilter filter = createFilter(filterPattern);
List<Page> allPages = createPages(filter, pages);
//fix progressbar max, which is dependent on the previous two lists
int steps = getNumberOfSteps(pages.size(), allPages.size(), converterStrings.size(), converters.size(), sendToConfluence);
this.state.updateMax(steps);
//convert the files
if (convertPages(allPages, converters)) {
//in case converting the pages disqualified some pages, we need to break if there are no pages left
if (allPages.size() < 1) {
String message = "All pages submitted were disqualified for various reasons. Could not complete conversion.";
log.warn(message);
this.errors.addError(Feedback.CONVERTER_ERROR, message, true);
this.state.updateMax(this.state.getStep()); //complete progress bar, prematurely
return;
}
//in case converting the pages disqualified some pages, we need to recompute progressbarmax
steps = getNumberOfSteps(pages.size(), allPages.size(), converterStrings.size(), converters.size(), sendToConfluence);
if (steps != this.state.getMax()) this.state.updateMax(steps);
// do final required conversions. This step is seperate, due to state saving issues
convertWithRequiredConverters(allPages);
//save pages if engine-saves-to-disk property is true. Useful for debugging.
//We are making this opt-in because users that don't need it will get a speed boost with fewer disk calls
if (Boolean.parseBoolean(this.miscProperties.getProperty(PROPKEY_ENGINE_SAVES_TO_DISK, "false")))
savePages(allPages, filterPattern);
else log.debug("Engine Saves To Disk setting turned off.");
//handling page histories and not sorting on create
if (isHandlingPageHistories() &&
!(isPageHistorySortOnCreate())) {
allPages = sortByHistory(allPages);
}
if (hierarchyHandler == HierarchyHandler.HIERARCHY_BUILDER && hierarchyBuilder != null) {
//tell the hierarchy builder about the page histories framework
//do this here so that we're sure the page histories properties are set
if (hierarchyBuilder.getProperties() != null) {
hierarchyBuilder.getProperties().setProperty("switch."+NONCONVERTERTYPE_PAGEHISTORYPRESERVATION, isHandlingPageHistories()+"");
if (getPageHistorySuffix() != null)
hierarchyBuilder.getProperties().setProperty("suffix."+NONCONVERTERTYPE_PAGEHISTORYPRESERVATION, getPageHistorySuffix());
}
//tell the hierarchy some other information
if (hierarchyBuilder.getProperties() != null) {
hierarchyBuilder.getProperties().setProperty("spacekey", settings.getSpace());
}
//build the hierarchy
HierarchyNode root = hierarchyBuilder.buildHierarchy(allPages);
int currenttotal = root.countDescendants()-1; //-1 for the null root;
log.debug("number of nodes in the hierarchy = " + root.countDescendants());
//upload pages, if the user approves
if (sendToConfluence && this.running) { //check here so that hierarchy can impact collisions without upload
if (Boolean.parseBoolean(this.miscProperties.getProperty("onlyorphans", "false"))) {
log.debug("Orphan attachments only.");
noteAttachments(root);
}
else {
writeHierarchy(root, currenttotal, settings.getSpace());
}
handleOrphanAttachments();
}
else if (!sendToConfluence){
log.debug("Send To Confluence setting turned off. --> Not uploading pages.");
}
} else { //no hierarchy
if (sendToConfluence && this.running) {//check here so that hierarchy can impact collisions without upload
writePages(allPages, settings.getSpace());
handleOrphanAttachments();
}
else if (!sendToConfluence){
log.debug("Send To Confluence setting turned off. --> Not uploading pages.");
}
}
//check for namespace collisions and emit errors if found
//(after hierarchy has had a chance to make changes)
listCollisions(allPages);
clearAttachedFileList();
}
log.info("Conversion Complete");
}
private void noteAttachments(HierarchyNode root) {
if (root.getPage() != null) {
log.debug("ORPHANDEBUG node: " + root.getPage().getName());
for (Attachment attachment : root.getPage().getAllAttachmentData()) {
alreadyAttached(root.getPage(), attachment.getFile());
if (root.getPage().getAncestors() != null) {
for (VersionPage ancestor : root.getPage().getAncestors()) {
for (Attachment ancAtt : ancestor.getAllAttachmentData()) {
log.debug("VERSION ORPHANDEBUG: " + ancestor.getName() + ": " + ancAtt.getFile().getName());
alreadyAttached(ancestor, ancAtt.getFile());
}
}
}
}
}
if (!root.getChildren().isEmpty()) {
for (HierarchyNode child : root.getChildren()) {
noteAttachments(child);
}
}
}
protected boolean isPageHistorySortOnCreate() {
return Boolean.parseBoolean(this.miscProperties.getProperty("page-history-sortoncreate", "true"));
}
/**
* handle any cleanup
*/
protected void initConversion() {
this.miscProperties.clear();
}
/**
* Instantiate all the converterStrings
*
* @param converterStrings a list of converter strings of the form "key=value"
* @return a list of converters
*/
public ArrayList<Converter> createConverters(List<String> converterStrings) {
return createConverters(converterStrings, true);
}
public ArrayList<Converter> createConverters(List<String> converterStrings, boolean runningState) {
String message = "Initializing Converters...";
if (runningState) this.state.updateNote(message);
log.info(message);
new DefaultXmlEvents().clearAll(); //everytime this method is called, have a clean slate of events
ArrayList<Converter> converters = new ArrayList<Converter>();
this.numNonConverterProperties = 0;
for (String converterStr : converterStrings) {
if (runningState) this.state.updateProgress();
if (runningState && !this.running) {
this.feedback = Feedback.CANCELLED;
return null;
}
Converter converter;
if (isNonConverterProperty(converterStr)) {
this.numNonConverterProperties++;
handleNonConverterProperty(converterStr);
continue;
}
converter = getConverterFromString(converterStr);
if (converter == null) {
continue;
}
converters.add(converter);
}
if (runningState) addDefaultMiscProperties();
return converters;
}
/**
* coverts pages with required converters
* @param pages
*/
protected void convertWithRequiredConverters(List<Page> pages) {
if (isIllegalHandlingEnabled()) {
//create pagename converter and convert with it
String pagenameConvStr = REQUIRED_CONVERTER_ILLEGAL_NAMES;
ArrayList<Converter> converters = createOneConverter(pagenameConvStr);
convertPages(pages, converters, "Checking for illegal pagenames.");
//create linkname converter and convert with it
String illegallinksConvStr = REQUIRED_CONVERTER_ILLEGAL_LINKS;
converters = createOneConverter(illegallinksConvStr);
convertPages(pages, converters, "Checking for links to illegal pagenames.");
}
}
/**
* converts the list of pages with the given converter
* @param pages list of pages to be converted
* @param useUI set this to false if you do not want the associated GUI elements
* to be updated. This is useful for unit testing.
*/
protected void convertWithRequiredConverters(List<Page> pages, boolean useUI) {
//XXX used only by the junit tests
//create pagename converter and convert with it
String pagenameConvStr = REQUIRED_CONVERTER_ILLEGAL_NAMES;
ArrayList<Converter> converters = createOneConverter(pagenameConvStr);
convertPages(pages, converters, "Checking for illegal pagenames.");
//get the state hashtable
IllegalPageNameConverter pagenameConverter = (IllegalPageNameConverter) converters.remove(0);
HashSet<String> illegalNames = pagenameConverter.getIllegalPagenames();
//create linkname converter and convert with it
String illegallinksConvStr = REQUIRED_CONVERTER_ILLEGAL_LINKS;
converters = createOneConverter(illegallinksConvStr);
IllegalLinkNameConverter linknameConverter = (IllegalLinkNameConverter) converters.get(0);
linknameConverter.setIllegalPagenames(illegalNames);
convertPages(pages, converters, "Checking for links to illegal pagenames.");
}
/**
* creates the arraylist of converters when only one converter is needed
* @param converterString string representing the converter. Should be in property format. Example:<br/>
* key=value
* @return arraylist with one converter as its sole item
*/
protected ArrayList<Converter> createOneConverter(String converterString) {
ArrayList<String> converterStrings = new ArrayList<String>();
converterStrings.add(converterString);
ArrayList<Converter> converters = createConverters(converterStrings);
return converters;
}
/**
* Instantiates a converter from a correctly formatted String.
* <p/>
* Note: This method is now only called once per converter -- first all converters
* are created, then all pages, then all converters are run on all pages.
*
* @param converterStr A string of the form "name.keyword=parameters". The
* keyword is used to create the correct type of converter, and the parameters
* are then passed to the converter. Finally, the "name.keyword" part is set as
* the key in the converter, mainly for debugging purposes.
* @return converter or null if no converter can be parsed/instantiated
*/
public Converter getConverterFromString(String converterStr) {
Converter converter;
int equalLoc = converterStr.indexOf("=");
String key = converterStr.substring(0, equalLoc);
String value = converterStr.substring(equalLoc + 1);
try {
if (key.indexOf(CONVERTERTYPE_CLASS) >= 0) {
converter = getConverterClassFromCache(value);
} else if (key.indexOf(CONVERTERTYPE_PERL) >= 0) {
converter = PerlConverter.getPerlConverter(value);
converter.setValue(value);
} else if (key.indexOf(CONVERTERTYPE_JAVAREGEXTOKEN) >= 0) {
converter = JavaRegexAndTokenizerConverter.getConverter(value);
converter.setValue(value);
} else if (key.indexOf(CONVERTERTYPE_JAVAREGEX) >= 0) {
converter = JavaRegexConverter.getConverter(value);
converter.setValue(value);
} else if (key.indexOf(CONVERTERTYPE_TWIKICLEANER) >= 0) {
//converter = getConverterClassFromCache(value);
converter = TWikiRegexConverterCleanerWrapper.getTWikiRegexConverterCleanerWrapper(value);
converter.setValue(value);
} else {
String note = "Converter ignored -- name pattern not recognized: " + key;
this.errors.addError(Feedback.BAD_PROPERTY, note, true);
log.error(note);
return null;
}
converter.setProperties(this.miscProperties);
if (converter instanceof RequiresEngineConverter) {
((RequiresEngineConverter) converter).setEngine(this);
}
} catch (ClassNotFoundException e) {
this.errors.addError(Feedback.BAD_CONVERTER_CLASS, "Converter ignored -- the Java class " + value + " was not found", true);
return null;
} catch (IllegalAccessException e) {
this.errors.addError(Feedback.BAD_CONVERTER_CLASS, "Converter ignored -- there was a problem creating a converter object", true);
return null;
} catch (InstantiationException e) {
this.errors.addError(Feedback.BAD_CONVERTER_CLASS, "Converter ignored -- there was a problem creating the Java class " + value, true);
return null;
} catch (ClassCastException e) {
this.errors.addError(Feedback.BAD_CONVERTER_CLASS, "Converter ignored -- the Java class " + value +
" must implement the " + Converter.class.getName() + " interface!", true);
return null;
}
converter.setKey(key);
return converter;
}
/**
* handles necessary state changes for expected properties
* that were set in the converter properties file.
* expected nonconverter properties include hierarchy builder properties
* and page history preservation properties
* @param converterStr should be a line from the converter properties file
* Example:
* MyWiki.0001.someproperty.somepropertytype=setting
* <br/>
* where somepropertytype is an expected property type:
* <br/>
* NONCONVERTERTYPE_HIERARCHYBUILDER or NONCONVERTERTYPE_PAGEHISTORYPRESERVATION
* or NONCONVERTERTYPE_ILLEGALHANDLING
* or NONCONVERTERTYPE_AUTODETECTSPACEKEYS
* or NONCONVERTERTYPE_FILTERS
* or NONCONVERTERTYPE_MISCPROPERTIES
*/
protected void handleNonConverterProperty(String converterStr) {
int equalLoc = converterStr.indexOf("=");
String key = converterStr.substring(0, equalLoc);
String value = converterStr.substring(equalLoc + 1);
String parent = "";
try {
if (key.indexOf(NONCONVERTERTYPE_HIERARCHYBUILDER) >= 0) {
if (isHierarchySwitch(key))
setHierarchyHandler(value);
else {
parent = HierarchyBuilder.class.getName();
Class c;
c = Class.forName(value);
HierarchyBuilder hierarchy = (HierarchyBuilder) c.newInstance();
hierarchyBuilder = hierarchy;
this.hierarchyBuilder.setProperties(this.miscProperties);
}
}
else if (key.endsWith(NONCONVERTERTYPE_PAGEHISTORYPRESERVATION)) {
handlePageHistoryProperty(key, value);
}
else if (key.endsWith(NONCONVERTERTYPE_ILLEGALHANDLING)) {
handleIllegalHandling(key, value);
}
else if (key.endsWith(NONCONVERTERTYPE_AUTODETECTSPACEKEYS)) {
handleAutoDetectSpacekeys(key, value);
}
else if (key.endsWith(NONCONVERTERTYPE_MISCPROPERTIES)) {
handleMiscellaneousProperties(key, value);
}
else if (key.endsWith(NONCONVERTERTYPE_FILTERS)) {
parent = FileFilter.class.getName();
handleFilters(key, value);
}
else if (key.endsWith(NONCONVERTERTYPE_XMLEVENT)) {
handleXmlEvents(key, value);
}
} catch (ClassNotFoundException e) {
String message = "Property ignored -- the Java class " + value + " was not found";
log.error(message);
this.errors.addError(Feedback.BAD_PROPERTY, message, true);
} catch (IllegalAccessException e) {
String message = "Property ignored -- there was a problem creating the Java class: " + value +
".\n" +
"Note: A necessary method's permissions were too restrictive. Check the constructor. ";
log.error(message);
this.errors.addError(Feedback.BAD_PROPERTY, message, true);
} catch (InstantiationException e) {
String message = "Property ignored -- there was a problem creating the Java class " + value +
".\n" +
"Note: The class cannot be instantiated as it is abstract or is an interface.";
log.error(message);
this.errors.addError(Feedback.BAD_PROPERTY, message, true);
} catch (ClassCastException e) {
String message = "Property ignored -- the Java class " + value +
" must implement the " + parent + " interface!";
log.error(message);
this.errors.addError(Feedback.BAD_PROPERTY, message, true);
} catch (IllegalArgumentException e) {
String message = "Property ignored -- property value was not in expected format.";
log.error(message);
this.errors.addError(Feedback.BAD_PROPERTY, message, true);
}
}
/**
* at long last making some performance enhancements
* here we are creating an object cache which should help a bit
*
* @param key A string representing the converter (actually the part after the
* equals sign of the converter string).
* @return
* @throws ClassNotFoundException
* @throws IllegalAccessException
* @throws InstantiationException
*/
private Converter getConverterClassFromCache(String key) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Converter converter = converterCacheMap.get(key);
if (converter == null) {
Class c = Class.forName(key);
converter = (Converter) c.newInstance();
converterCacheMap.put(key, converter);
}
return converter;
}
/**
* creates file filter.
* If we have no filter values, returns null.
* If we have at least one filter value, uses the FilterChain class
* to create FileFilter that will handle all of the filter requirements.
* There are two types of supported filters: Class filters, and endswith filters.
* Class filters are fully resolved class names for classes that implement FileFilter.
* Endswith filters are text strings that the end of the filename must conform to.
* If there are more than one filter invoked, the following will be used to resolve
* which files to accept: Only pages that all class filters accept as long as
* any endswith filter accepts as well will be included. (Class filters are ANDed.
* Endswith filters are ORed.) Example: If you had two endswiths, and a class: ".txt",
* ".xml", and NoSvnFilter, then .txt and .xml files that the NoSvnFilter accepts
* will be included.
* @return FileFilter or null
*/
protected FileFilter createFilter(final String pattern) {
Set<String> values = getFilterValues();
if (pattern != null && !"".equals(pattern))
values.add(pattern);
if (values.isEmpty()) return null;
FilterChain chain = new FilterChain(values, this.miscProperties);
return chain.getFilter();
}
/**
* Creates PageForXmlRpcOld objects for all the files in inputPages.
*
* @param filter file filter to be used to filter input pages, or null, if no such filter should be used
* @param inputPages A list of files and directories that Pages should be created for.
* @return A list of PageForXmlRpcOld objects for all files matching the pattern in the settings.
*/
protected List<Page> createPages(FileFilter filter, List<File> inputPages) {
String message = "Initializing Pages...";
this.state.updateNote(message);
log.info(message);
List<Page> allPages = new LinkedList<Page>();
for (File fileOrDir : inputPages) {
this.state.updateProgress();
List<Page> pages = recurse(fileOrDir, filter);
setupPages(fileOrDir, pages);
allPages.addAll(pages);
}
log.debug("Number of page inputs (all): " + allPages.size());
if (isHandlingPageHistories() && isPageHistorySortOnCreate()) {
List<Page> sorted = sortByHistory(allPages);
log.debug("Number of page inputs (sorted): " + sorted.size());
return sorted;
}
return allPages;
}
/**
* Recurses through a directory structure and adds all files in it matching the filter.
* Called by createPages.
*
* @param fileOrDir A directory or file. Must not be <code>null</code>.
* @param filter the filter to use when selecting files
* @return A list with PageForXmlRpcOld objects for all the matching files in the directory and its subdirectories
*/
private List<Page> recurse(File fileOrDir, FileFilter filter) {
assert fileOrDir != null;
List<Page> result = new LinkedList<Page>();
if (fileOrDir.isFile()) { //it's a file AND
if (filter == null || filter.accept(fileOrDir)) { //there's no filter OR the filter accepts the file
PageSplitter splitter = getPageSplitter();
if (splitter == null)
result.add(new Page(fileOrDir));
else
result.addAll(splitter.split(fileOrDir));
}
else
log.debug("Filtering out filename: " + fileOrDir.getName());
} else if (fileOrDir.isDirectory()) {
File[] files = fileOrDir.listFiles(filter);
for (File file : files) {
result.addAll(recurse(file, filter));
}
}
else { //some other problem
String message = "Could not find file: '" +
fileOrDir.getAbsolutePath() +
"'.\n" +
"Check existence and permissions.";
log.warn(message);
this.errors.addError(Feedback.BAD_FILE, message, true);
}
return result;
}
private PageSplitter getPageSplitter() {
String classname = this.miscProperties.getProperty("pagesplitter", null);
if (classname == null) return null;
Class c;
try {
c = Class.forName(classname);
} catch (ClassNotFoundException e) {
log.error("Could not find pagesplitter class named: " + classname, e);
return null;
}
try {
PageSplitter splitter = (PageSplitter) c.newInstance();
return splitter;
} catch (InstantiationException e) {
log.error("Could not instantiate pagesplitter class named: " + classname, e);
} catch (IllegalAccessException e) {
log.error("Pagesplitter class can not legally be accessed: " + classname, e);
}
return null;
}
/**
* Set the names of the pages and performs any other setup needed. Called by recurse().
* If the user selected a directory and this file is inside it, the base directory's
* path is removed and the rest is used as the page name.
* <p/>
* Any directory separators are replaced with the constant CONFLUENCE_SEPARATOR.
*
* @param baseDir The directory that the top-level documents are in
* @param pages A list of pages to set up
*/
protected void setupPages(File baseDir, List<Page> pages) {
String basepath = baseDir.getParentFile().getPath() + File.separator;
int baselength = basepath.length();
for (Page page : pages) {
String pagePath = page.getFile().getPath();
String pageName = getPagename(pagePath.substring(baselength));
log.debug("New page: " + pageName + " -> " + pagePath);
//Strip the file name from the path.
String path = getPath(pagePath);
page.setPath(path);
page.setName(pageName);
if (isHandlingPageHistoriesFromFilename()) preserveHistory(page, pageName);
}
}
/**
* figures out path var for Page based on complete path to page's file
* @param pagePath
* @return
*/
private String getPath(String pagePath) {
int fileNameStart = pagePath.lastIndexOf(File.separator);
if (fileNameStart >= 0) {
pagePath = pagePath.substring(0, fileNameStart);
} else {
pagePath = "";
}
return pagePath;
}
/**
* uses the filename to set the version and name of the given page
* so that the history is preserved in the conversion. Note:
* uses the pageHistorySuffix which is set by the handlePageHistoryProperty
* method
* @param page object that will be changed to reflect pagename and version of given filename
* @param filename should use the pageHistorySuffix to indicate version and pagename:
* <br/>
* if pageHistorySuffix is -#.txt
* <br/>
* then filename should be something like: pagename-2.txt
* @return Page with changed name and version
* Will return passed page with no changes if:
* <ul>
* <li>suffix is null</li>
* <li> suffix has no numerical indicator (#)</li>
* </ul>
*/
protected Page preserveHistory(Page page, String filename) {
if (loadOnAncestors()) {
addAncestors(page);
if (!page.getAncestors().isEmpty()) {
page.setVersion(page.getLatestVersion()+1);
log.debug("Current page version: " + page.getVersion());
}
return page;
}
return identifyHistoryOnePage(page, filename);
}
public Page identifyHistoryOnePage(Page page, String filename) {
//get suffix
String suffix = getPageHistorySuffix();
if (suffix == null) {
log.error("Error attempting to preserve history: Page history suffix is Null.");
return page;
}
//create regex for filename based on the suffix
Matcher hashFinder = hashPattern.matcher(suffix);
String suffixReplaceRegex = "";
if (hashFinder.find()) {
suffixReplaceRegex = hashFinder.replaceAll("(\\\\d+)");
suffixReplaceRegex = "(.*)" + suffixReplaceRegex;
}
else {
log.error("Error attempting to preserve history: Suffix is invalid. Must contain '#'.");
return page;
}
//get the version and name
Pattern suffixReplacePattern = Pattern.compile(suffixReplaceRegex);
Matcher suffixReplacer = suffixReplacePattern.matcher(filename);
if (suffixReplacer.find()) {
String pagename = suffixReplacer.group(1);
String versionString = suffixReplacer.group(2);
page.setName(pagename); //set name before version so latestversion data is properly set in Page
if (Boolean.parseBoolean(this.miscProperties.getProperty("page-history-sortwithtimestamp", "false")))
page.setTimestamp(new Date(Long.parseLong(versionString)*1000));
else
page.setVersion(Integer.parseInt(versionString));
}
return page;
}
/* Page History Load on Ancestors methods - START */
private boolean loadOnAncestors() {
return Boolean.parseBoolean(this.miscProperties.getProperty("page-history-load-as-ancestors", "false"));
}
private void addAncestors(Page page) {
String ancestorDir = this.miscProperties.getProperty("page-history-load-as-ancestors-dir", null);
if (ancestorDir == null) {
log.warn("page-history-load-as-ancestors-dir must be set. Cannot add ancestors.");
return;
}
String relPath = getPageRelativePath(page);
if (!ancestorDir.endsWith(File.separator) && !relPath.startsWith(File.separator))
ancestorDir += File.separator;
String ancestorPath = ancestorDir + relPath;
File dir = new File(ancestorPath);
File[] allFiles = dir.listFiles();
for (File file : allFiles) {
String filename = file.getName();
Page newPage = new VersionPage(file);
newPage.setParent(page);
newPage = identifyHistoryOnePage(newPage, filename);
if (newPage.getName() == null) continue;
if (newPage.getName().equalsIgnoreCase(page.getName().replaceFirst("[.][^.]+$", ""))) {
newPage.setName(page.getName()); //we need them to have the same name for latestversion to work
log.debug("Found ancestor page: " + newPage.getFile().getPath());
newPage.setPath(getPath(newPage.getFile().getPath()));
page.addAncestor((VersionPage) newPage);
}
}
Collections.sort(page.getAncestors());
if (!page.getAncestors().isEmpty() &&
Boolean.parseBoolean(this.miscProperties.getProperty("page-history-sortwithtimestamp", "false"))) {
if (Boolean.parseBoolean(this.miscProperties.getProperty("page-history-load-as-ancestors-lastiscurrent", "false"))) {
page.getAncestors().remove(page.getAncestors().lastElement());//remove the last ancestor if its the same as current
}
for (int i = 1; i < page.getAncestors().size(); i++) {
VersionPage version = page.getAncestors().get(i);
version.setVersion(version.getLatestVersion()+1);
}
page.setSortWithTimestamp(true); //affects sorting of collections of pages (including hierarchies)
}
if (this.miscProperties.containsKey("page-history-maxversions")) { //useful for debugging
String maxString = this.miscProperties.getProperty("page-history-maxversions", null);
int max = Integer.parseInt(maxString);
if (max <= page.getAncestors().size()) {
log.debug("number of ancestors: " + page.getAncestors().size());
int actmax = page.getAncestors().size();
for (int i = actmax-1; i >=max; i--) {
page.getAncestors().remove(i);
}
log.debug("after limiting, number of ancestors: " + page.getAncestors().size());
}
}
}
protected String getPageRelativePath(Page page) {
String ignorable = this.miscProperties.getProperty("filepath-hierarchy-ignorable-ancestors", "");
String full = page.getPath();
if (full == null) return null;
return full.replaceAll("\\Q"+ignorable + "\\E", "");
}
/* Page History Load on Ancestors methods - END */
/**
* gets the pagename given the pagepath
* @param pagePath
* @return pagename
*/
protected String getPagename(String pagePath) {
String pageName = "";
if (hierarchyHandler == HierarchyHandler.DEFAULT ||
hierarchyHandler == HierarchyHandler.HIERARCHY_BUILDER) {
pageName = pagePath.substring(pagePath.lastIndexOf(File.separator) + 1);
} else if (hierarchyHandler == HierarchyHandler.PAGENAME_HIERARCHIES) {
String quotedSeparator = Pattern.quote(File.separator);
pageName = pagePath.replaceAll(quotedSeparator, CONFLUENCE_SEPARATOR);
}
return pageName;
}
/**
* converts the given pages with the given converts
* @param pages
* @param converters
* @return true if conversion of all pages succeeded
*/
protected boolean convertPages(List pages, List<Converter> converters) {
return convertPages(pages, converters, "Converting pages...");
}
/**
* converts the given pages with the given converters
* @param pages
* @param converters
* @param note, message for the progress monitor
* @return true if conversion of all pages succeeded
*/
protected boolean convertPages(List<Page> pages, List<Converter> converters, String note) {
boolean result = true;
this.state.updateNote(note);
log.info(note);
this.startTotalConvertTime = (new Date()).getTime();
//go through each page
for (Iterator<Page> iter = pages.iterator(); iter.hasNext();) {
if (!this.running) {
this.feedback = Feedback.CANCELLED;
return false;
}
Page page = iter.next();
//some bookkeeping
long startTimeStamp = conversionBookkeepingNextPage(page);
//get the file's contents
if (page.getOriginalText() == null || "".equals(page.getOriginalText())) {
File file = getFileContents(page);
if (file == null) {
iter.remove(); //get rid of this page from the iterator.
continue;
}
} //else we used a PageSplitter to set the original text, so we can go straight to conversion
//convert the page
convertPage(converters, page);
//more bookkeeping
conversionBookkeepingEndThisPage(startTimeStamp);
if (!this.running) {
this.feedback = Feedback.CANCELLED;
return false;
}
if (page.getAncestors() != null && !page.getAncestors().isEmpty()) {
convertPages(page.getAncestors(), converters);
}
}
//still more bookkeeping
conversionBookkeepingEndAll(pages, converters);
return result;
}
/**
* make some log entries about the time it took to convert a page
* @param startTimeStamp
* @return
*/
private long conversionBookkeepingEndThisPage(long startTimeStamp) {
long stopTimeStamp = ((new Date()).getTime());
log.info(" time to convert " + (stopTimeStamp - startTimeStamp) + "ms");
return stopTimeStamp;
}
/**
* make some log entries regarding the length of time it took to do the entire conversion
* @param pages
* @param converters
*/
private void conversionBookkeepingEndAll(List<Page> pages, List<Converter> converters) {
long endTotalConvertTime = (new Date()).getTime();
long totalTimeToConvert = (endTotalConvertTime - startTotalConvertTime)/1000;
String baseMessage = "::: total time to convert files: "+ totalTimeToConvert+ " seconds.";
log.info(baseMessage);
String message = baseMessage +
"For " +
pages.size() +
" pages and using " +
converters.size() +
" converters.";
totalsFileLog.info(message);
}
/**
* update the progress monitor and write some log entries for this page
* @param page
* @return
*/
private long conversionBookkeepingNextPage(Page page) {
long startTimeStamp = ((new Date()).getTime());
log.info("-------------------------------------");
log.info("converting page file: " + page.getName());
if (page.getFile() != null && page.getFile().getName() != null)
log.debug("original file name: " + page.getFile().getName());
return startTimeStamp;
}
/**
* get the file of the given page
* @param page file for this page
* @return the file, or return null if not possible
*/
private File getFileContents(Page page) {
File file = page.getFile();
if (file == null) {
if (page.getOriginalText() != null && !"".equals(page.getOriginalText())) {
log.warn("This appears to be a unit test. Continue as for Unit Test.");
String path = page.getPath();
if (path == null) path = "";
file = new File(path);
}
else {
log.warn("No file was set for page " + page.getName() + ". Skipping page.");
return null;
}
}
else if (page.getOriginalText() == null){
try {
String pageContents = "";
if (isGzip() && page instanceof VersionPage) {
pageContents = getGzipText(file);
}
else pageContents = getAsciiText(file);
page.setOriginalText(pageContents);
} catch (IOException e) {
String message = "Could not read file " + file.getAbsolutePath() + ".\n" +
"Check existence and permissions.";
log.error(message);
this.errors.addError(Feedback.BAD_FILE, message, true);
return null;
}
// Save the true source since the original will get modified in convert.
page.setUnchangedSource(page.getOriginalText());
}
return file;
}
private boolean isGzip() {
return Boolean.parseBoolean(this.miscProperties.getProperty("page-history-load-as-ancestors-isgzip", "false"));
}
private String getGzipText(File file) throws IOException {
if (changingEncoding()) {
log.error("Changing Encoding from Gzip file is not supported yet! Can't change encoding");
}
return FileUtils.readGzipFile(file);
}
public String getAsciiText(File file) throws IOException,
UnsupportedEncodingException {
String pageContents;
if (changingEncoding()) {
String encoding = getEncoding();
byte[] pagebytes = FileUtils.getBytesFromFile(file);
try {
pageContents = new String(pagebytes, encoding);
} catch (UnsupportedEncodingException e) {
String baseerror = "Could not encode file with encoding: " + encoding + ".";
log.error(baseerror + " Using utf-8.");
this.errors.addError(Feedback.BAD_SETTING, baseerror, true);
pageContents = new String(pagebytes, "utf-8");
}
}
else pageContents = FileUtils.readTextFile(file);
return pageContents;
}
private boolean changingEncoding() {
if (this.miscProperties != null)
return this.miscProperties.containsKey("encoding");
return false;
}
private String getEncoding() {
if (this.miscProperties != null)
return this.miscProperties.getProperty("encoding", "utf-8");
return "utf-8";
}
/**
* converts one page with the given converters
* @param converters list of converters
* @param page page object
*/
protected Page convertPage(List<Converter> converters, Page page) {
if (page.getConvertedText() == null)
page.setConvertedText(page.getOriginalText()); //in case empty converter list
for (Converter converter : converters) {
try {
this.state.updateProgress();
if (this.settings != null) {
converter.setAttachmentDirectory(this.settings.getAttachmentDirectory());
}
else {
//for backwards compatibility with v2
ConfluenceSettingsForm confSettings = UWCForm2.getInstance().getConfluenceSettingsForm();
converter.setAttachmentDirectory(confSettings.getAttachmentDirectory());
}
converter.convert(page);
// Need to reset originalText here because each converted expects
// to start with the result of previous conversions.
page.setOriginalText(page.getConvertedText());
} catch (Exception e) {
String note = "Exception thrown by converter " + converter.getKey() +
" on page " + page.getName() + ". Continuing with next converter.";
log.error(note, e);
this.errors.addError(Feedback.CONVERTER_ERROR, note, true);
}
if (converter.getErrors().hasErrors()) {
this.hadConverterErrors = true;
this.state.updateNote(converter.getErrors().getFeedbackWindowErrorMessages());
}
}
return page;
}
/**
* Write pages to disk. They are saved to the directory output/output below the
* current working directory.
*
* @param pages The pages to save
* @param pattern This file name extension is appended
* to each page name to create the file name.
*/
private void savePages(List<Page> pages, String pattern) {
String message = "Saving Pages to Filesystem";
this.state.updateNote(message);
log.info(message);
FileUtils.createOutputDirIfNeeded();
String outputDirName = UWCGuiModel.getOutputDir();
log.debug("Output Directory = " + outputDirName);
File outputDir = new File(outputDirName);
if (!outputDir.exists() && !outputDir.mkdir()) {
String dirfailMessage = "Directory creation failed for directory " + outputDirName;
log.error(Feedback.BAD_OUTPUT_DIR + ": " + dirfailMessage);
this.errors.addError(Feedback.BAD_OUTPUT_DIR, dirfailMessage, true);
}
for (Page page : pages) {
if (!this.running) {
this.feedback = Feedback.CANCELLED;
return;
}
this.state.updateProgress();
String outputFileLoc = outputDirName + File.separator + page.getName() + pattern;
FileUtils.writeFile(page.getConvertedText(), outputFileLoc);
}
}
protected Vector listCollisions(List<Page> pages) {
Vector<String> collisions = new Vector<String>();
//check to see if "off" property is present
if (this.miscProperties != null &&
this.miscProperties.containsKey("list-collisions") &&
!Boolean.parseBoolean(this.miscProperties.getProperty("list-collisions"))) {
log.debug("Namespace Collisions Feature turned off.");
return collisions;
}
//sort
Vector<Page> sorted = new Vector<Page>();
sorted.addAll(pages);
AsciiVersionComparator version = new AsciiVersionComparator();
Collections.sort(sorted, version);
//look for collisions
Page last = new Page(null);
last.setName("");
last.setPath("");
for (int i = 1; i < sorted.size(); i++) {
Page page1 = (Page) sorted.get(i-1);
Page page2 = (Page) sorted.get(i);
log.debug("Checking for collisions: " + page1.getName() + " and " + page2.getName());
String collision = "";
//if each page lower cased is the same
if (colliding(page1, page2)) {
if (getCollisionComparisonString(page1).equals(getCollisionComparisonString(last))) { //already have one for this name
String latestPath = getPagePath(page2);
String current = collisions.remove(collisions.size()-1);
collision = current + ", " + latestPath + page2.getName();
}
else { //starting here
collision = getPagePath(page1) + page1.getName() + ", " +
getPagePath(page2) + page2.getName();
}
collisions.add(collision);
last = page1;
String error = "Potential namespace collision detected for pages: " + collision;
this.getErrors().addError(Feedback.NAMESPACE_COLLISION,
error,
true);
this.log.error(error);
}
}
return collisions;
}
protected boolean colliding(Page page1, Page page2) {
boolean name = getCollisionComparisonString(page1).equals(getCollisionComparisonString(page2));
boolean version = page1.getVersion() == page2.getVersion();
boolean space = false;
if (page1.getSpacekey() != null) {
space = page1.getSpacekey().equals(page2.getSpacekey());
}
else if (page2.getSpacekey() != null) {
space = page2.getSpacekey().equals(page1.getSpacekey());
}
else if (page1.getSpacekey() == null && page2.getSpacekey() == null)
space = true;
boolean path = !getPagePath(page1).equals(getPagePath(page2));
return name
&& version //and same page history version
&& space// and same space
&& path; // but not the same path
}
/**
* @param page
* @return the namespace collision comparison string. Either
* the pagename, lower cased, or if we're using the auto detect spacekeys
* feature, then the page page + the pagename, all lower cased.
*/
protected String getCollisionComparisonString(Page page) {
if (this.autoDetectSpacekeys)
return (getPagePath(page) + page.getName()).toLowerCase();
return page.getName().toLowerCase();
}
/**
* @param page
* @return the page's path, including an ending file seperator if one isn't
* already there
*/
private String getPagePath(Page page) {
return (page.getPath().endsWith(File.separator)?
page.getPath():
page.getPath()+File.separator);
}
/**
* Writes the pages to Confluence. If the process takes more than three seconds,
* a progress monitor will be displayed so that the user can see that something is
* indeed happening.
*
* @param pages The pages to output.
* @param spacekey space to which the pages will be written
*/
protected void writePages(List pages, String spacekey) {
writePages(pages, spacekey, true);
}
protected void writePages(List pages, String spacekey, boolean logging) {
if (logging) {
String note = "Uploading Pages to Confluence...";
this.state.updateNote(note);
log.info(note);
}
int numUploaded = 0;
List<Page> casted = (List<Page>) pages;
// at last, write the pages to Confluence!
for (Page page : casted) {
this.state.updateProgress();
if (!this.running) {
this.feedback = Feedback.CANCELLED;
return;
}
if (sendPage(page, null, this.settings) == null) continue;
numUploaded++;
if (logging && (numUploaded % 10) == 0) {
String message = "Uploaded " + numUploaded +
" out of " + pages.size() +
" page"+ (numUploaded==1?"":"s") +
".";
this.state.updateNote(message);
log.info(message);
}
}
if (logging) {
String message = "Uploaded " + numUploaded +
" out of " + pages.size() +
" page"+ (numUploaded==1?"":"s") +
".";
this.state.updateNote(message);
log.info(message);
}
}
protected void clearAttachedFileList() {
//attachedFiles is cleared so that if we do another conversion
//without closing the UWC, it won't think the attachment has already been
//attached
log.debug("Clearing Attached Filelist for next run.");
this.attachedFiles = null;
this.attachedPaths = null;
}
Pattern uploadedPattern = Pattern.compile("Attachment Uploaded: (.*)");
public void handleOrphanAttachments() {
if (this.settings.getUploadOrphanAttachments().equalsIgnoreCase("true"))
{
if (this.miscProperties.containsKey("attachments-uploaded-file")) {
identifyPreviouslyAttachedPaths(this.miscProperties.getProperty("attachments-uploaded-file"));
}
ArrayList <File> orphanAttachments=findOrphanAttachments(this.settings.getAttachmentDirectory());
uploadOrphanAttachments(orphanAttachments);
}
}
protected void identifyPreviouslyAttachedPaths(String uploadedpath) {
if (uploadedpath != null && !"".equals(uploadedpath)) {
File uploadedFile = new File(uploadedpath);
if (uploadedFile.exists()) {
try {
String uploaded = FileUtils.readTextFile(uploadedFile);
Matcher fileFinder = uploadedPattern.matcher(uploaded);
while (fileFinder.find()) {
String alreadyFound = fileFinder.group(1);
if (this.attachedPaths == null) this.attachedPaths = new HashSet<String>();
this.attachedPaths.add(alreadyFound);
}
} catch (IOException e) {
log.error("Could not open: " + uploadedpath, e);
}
}
}
}
//for unit testing purposes
protected void addAttachedPath(String path) {
if (this.attachedPaths == null) this.attachedPaths = new HashSet<String>();
this.attachedPaths.add(path);
}
//for unit testing purposes
protected void clearAttachedPath() {
this.attachedPaths = null;
}
/******
* to find all orphan attachment files
* @param dirName
* @return
*/
protected ArrayList<File> findOrphanAttachments(String dirName)
{
ArrayList<File> orphanAttachments=new ArrayList<File>();
File directory=new File(dirName);
for (File file : directory.listFiles()) {
if (file.isDirectory())
orphanAttachments.addAll(findOrphanAttachments(file.getAbsolutePath()));
else if (file.isFile())
{
if (orphanAlreadyAttached(file))
continue;
log.debug("Found orphan attachment: " + file.getAbsolutePath());
orphanAttachments.add(file);
}
}
return orphanAttachments;
}
/**
* to upload the orphan attachment files to wiki.
*/
protected void uploadOrphanAttachments(ArrayList<File> orphanAttachments)
{
if (orphanAttachments == null || orphanAttachments.size() == 0)
return;
Hashtable pageTable = new Hashtable();
pageTable.put("content", "");
pageTable.put("title", ORPHAN_ATTACHMENTS_PAGE_TITLE);
//create ConfluenceServerSettings object
ConfluenceServerSettings confSettings = new ConfluenceServerSettings();
confSettings.login = settings.getLogin();
confSettings.password = settings.getPassword();
confSettings.url = settings.getUrl();
confSettings.spaceKey = settings.getSpace();
confSettings.truststore = settings.getTruststore();
confSettings.trustpass = settings.getTrustpass();
confSettings.trustallcerts = settings.getTrustall();
RemoteWikiBroker broker = RemoteWikiBroker.getInstance();
//check for problems with settings
checkConfluenceSettings(confSettings);
//send page
String pageId = sendPage(broker, pageTable, confSettings);
int total=orphanAttachments.size();
int count=0;
for (File file : orphanAttachments) {
sendAttachment(file, broker, pageId, confSettings);
count++;
if ((count % 10) == 0) {
String message = "Uploaded " + count +
" out of " + total + " orphan attachments.";
this.state.updateNote(message);
log.info(message);
}
}
String message = "Uploaded " + total + " orphan attachments.";
this.state.updateNote(message);
log.info(message);
}
/****
* send an attachment file to wiki.
*
* @param file
* @param broker
* @param pageId
* @param confSettings
* @return the attachment object we sent. used by junit tests
*/
protected AttachmentForXmlRpc sendAttachment(File file, RemoteWikiBroker broker, String pageId, ConfluenceServerSettings confSettings) {
return sendAttachment(new Attachment(file), broker, pageId, confSettings);
}
protected AttachmentForXmlRpc sendAttachment(Attachment attachment, RemoteWikiBroker broker, String pageId, ConfluenceServerSettings confSettings)
{
File file = attachment.getFile();
AttachmentForXmlRpc attachmentRpc = new AttachmentForXmlRpc();
if (tooBig(file) || doesNotExist(file))
return null;
attachmentRpc.setFileName(attachment.getName());
attachmentRpc.setFileLocation(file.getAbsolutePath());
attachmentRpc.setContentType(determineContentType(file)); //XXX Note: if the filename is different from the file, the content type determining might be foiled
attachmentRpc.setComment(attachment.getComment() == null?getAttachmentUploadComment():attachment.getComment());
String errorMessage = "Couldn't send attachmentRpc " +
file.getAbsolutePath() + ". Skipping attachmentRpc.";
if (usingWebdav()) {
String webdavPath = getWebdavPath();
sendAttachmentWebdav(broker, pageId, confSettings, attachmentRpc, webdavPath, errorMessage);
}
else
sendAttachmentRemoteAPI(broker, pageId, confSettings, attachmentRpc, errorMessage);
attachmentLog.info("Attachment Uploaded: " + file.getAbsolutePath());
return attachmentRpc;//for junit tests
}
private String getAttachmentUploadComment() {
if (this.miscProperties != null &&
this.miscProperties.containsKey("attachment-upload-comment")) {
String comment = this.miscProperties.getProperty("attachment-upload-comment");
if (comment == null) return DEFAULT_ATTACHMENT_UPLOAD_COMMENT;
return comment;
}
return DEFAULT_ATTACHMENT_UPLOAD_COMMENT;
}
protected boolean usingWebdav() {
if (this.miscProperties != null &&
this.miscProperties.containsKey("attachments-use-webdav")) {
return Boolean.parseBoolean(
this.miscProperties.getProperty("attachments-use-webdav", "false"));
}
return false;
}
protected String getWebdavPath() {
if (this.miscProperties != null &&
this.miscProperties.containsKey("webdav-path")) {
return this.miscProperties.getProperty("webdav-path", RemoteWikiBroker.WEBDAV_PATH_EARLY);
}
return RemoteWikiBroker.WEBDAV_PATH_EARLY;
}
private void sendAttachmentWebdav(RemoteWikiBroker broker, String pageId,
ConfluenceServerSettings confSettings,
AttachmentForXmlRpc attachment, String basepath,
String errorMessage) {
try {
Map pagesByIdMap = broker.getAllServerPagesMapById(confSettings, confSettings.spaceKey);
String webdavPath = broker.getWebDAVPagePath(confSettings.url, confSettings.spaceKey, pageId, pagesByIdMap, basepath);
broker.sendFileViaWebDAV(attachment.getFileLocation(), webdavPath, confSettings.login, confSettings.password);
} catch (IOException e) {
log.error(Feedback.BAD_FILE + ": " + errorMessage, e);
this.errors.addError(Feedback.BAD_FILE, errorMessage, true);
} catch (XmlRpcException e) {
log.error(Feedback.REMOTE_API_ERROR + ": " + errorMessage, e);
this.errors.addError(Feedback.REMOTE_API_ERROR, errorMessage, true);
if (Pattern.matches(".*?You do not have the permissions.*", e.getMessage())) {
String noPermissionsError = "User '" + confSettings.login +
"' " +
"does not have permission to attach files to space '" +
confSettings.spaceKey +
"'.";
log.debug(Feedback.USER_NOT_PERMITTED + ": " + noPermissionsError);
this.errors.addError(Feedback.USER_NOT_PERMITTED, noPermissionsError, true);
}
}
}
private void sendAttachmentRemoteAPI(RemoteWikiBroker broker, String pageId, ConfluenceServerSettings confSettings, AttachmentForXmlRpc attachment, String errorMessage) {
try {
broker.storeAttachment(confSettings, pageId, attachment);
} catch (IOException e) {
log.error(Feedback.BAD_FILE + ": " + errorMessage, e);
this.errors.addError(Feedback.BAD_FILE, errorMessage, true);
} catch (XmlRpcException e) {
log.error(Feedback.REMOTE_API_ERROR + ": " + errorMessage, e);
this.errors.addError(Feedback.REMOTE_API_ERROR, errorMessage, true);
if (Pattern.matches(".*?You do not have the permissions.*", e.getMessage())) {
String noPermissionsError = "User '" + confSettings.login +
"' " +
"does not have permission to attach files to space '" +
confSettings.spaceKey +
"'.";
log.debug(Feedback.USER_NOT_PERMITTED + ": " + noPermissionsError);
this.errors.addError(Feedback.USER_NOT_PERMITTED, noPermissionsError, true);
}
}
}
/**
* Writes a hierarchy of pages to Confluence. The empty nodes (those with page=null) will
* not be written if there already exists a page in Confluence. If there is no page at the
* corresponding place in Confluence, an empty page will be created.
*
* Like writePages(), this method will show a progress bar if the hierarchy takes more than
* a few seconds to send to Confluence.
*
* @param root The root of the hierarchy. Note: The root node itself will <strong>NOT</strong> be
* added to Confluence. All it's children will be added as top-level pages in the space.
* @param maxProgress used by logging and status messages
* @param spacekey space to which the pages will be written
*/
protected void writeHierarchy(HierarchyNode root, int maxProgress, String spacekey) {
String message = "Uploading Pages to Confluence...";
this.state.updateNote(message);
log.info(message);
int progressNum = 0;
this.newNodes = 0; //this has to be a field, because we're already returning something;
// at last write the pages to Confluence!
for (HierarchyNode topLevelPage : root.getChildren()) {
log.debug("writeHierarchy: toplevelpage = " + topLevelPage.getName());
log.debug("number of children this toplevelpage has = " + topLevelPage.getChildren().size());
progressNum = writeHierarchy(topLevelPage, null, progressNum, maxProgress, spacekey);
if (!this.running) {
this.feedback = Feedback.CANCELLED;
return;
}
}
}
/**
* This is the recursive part of <code>writeHierarchy</code>. Don't call this directly!
* Call writeHierarchy(root) instead.
*
* @param node The current node in the hierarchy
* @param parentId The Confluence "page ID" of the parent page
* @param progress The number of pages that have been converted so far
* (used to keep the progress monitor updated)
* @param spacekey space to which the pages will be written
* @return The number of pages converted after this node and all its descendants have been added.
*/
private int writeHierarchy(
HierarchyNode node,
String parentId,
int progress,
int maxProgress,
String spacekey) {
if (!this.running) {
this.feedback = Feedback.CANCELLED;
return progress;
}
// First upload the page contained in this node
Page page = node.getPage();
// create missing nodes - like a directory that didn't have a corresponding page
if (page == null) {
// This node is a "placeholder" because there are pages further down the hierarchy but
// for some reason this node was not included in the conversion. Create an empty page.
// Note that this page will only be sent to Confluence if there was no page in place before.
page = new Page(null);
page.setName(node.getName());
page.setOriginalText("");
page.setConvertedText("");
page.setPath(node.getName()); //needed by auto-detect spacekeys feature
String message = "Page '" + page.getName() + "' does not exist. Creating it now.";
log.info(message);
this.state.updateNote(message);
this.newNodes++;
this.state.updateMax(this.state.getMax() + 1);
}
//upload the page
String myId = sendPage(page, parentId, this.settings);
//some bookkeeping
progress++;
logProgressMessage(progress, maxProgress);
// Then recursively upload all the node's descendants
for (HierarchyNode child : node.getChildren()) {
progress = writeHierarchy(child, myId, progress, maxProgress, spacekey);
if (!this.running) {
this.feedback = Feedback.CANCELLED;
return progress;
}
}
return progress;
}
/**
* sends upload progress messages to the feedback window and the log
* @param current current number of uploaded pages
* @param max max number of uploaded pages
*/
private void logProgressMessage(int current, int max) {
this.state.updateProgress();
String message = "Uploaded " + current + " out of " + (max + this.newNodes) + " pages.";
//more visible note if current is divisible by 10 or last page
if ((current % 10) == 0 || current == (max + this.newNodes)) {
this.state.updateNote(message);
log.info(message);
}
else { //less visible note for everything else
log.debug(message);
}
}
/**
* sends page, using settings from the given settings
* @param page
* @param parentId
* @return page id
* @throws IllegalArgumentException if a confluenceSetting is invalid
*/
protected String sendPage(Page page, String parentId, UWCUserSettings settings) {
//write current page
//XXX why are we setting these up every page. Most of these are global.
//XXX If we set these up earlier in the process, we could do the checkConfluenceSettings call
//(currently in the next sendPage) earlier in the process as well
ConfluenceServerSettings confSettings = getConfluenceServerSettings(settings);
//check to see if we've assigned a space to the page
if (page.getSpacekey() != null && !"".equals(page.getSpacekey())) {
confSettings.spaceKey = page.getSpacekey();
String[] spacedata = page.getSpaceData(page.getSpacekey());
String spacename = (spacedata == null || spacedata.length < 1)?page.getSpacekey():spacedata[0];
String spacedesc = (spacedata == null || spacedata.length < 2)?"":spacedata[1];
if (!createSpace(confSettings, spacename, spacedesc, page.isPersonalSpace(), page.getPersonalSpaceUsername())) {
log.warn("Could not create space '" + confSettings.spaceKey + "' assigned to page '" + page.getName() + "'. " +
"Using default space from settings.");
confSettings.spaceKey = settings.getSpace();
}
} // check to see if we're automatically detecting spaces based on the file system
else if (isAutoDetectingSpacekeys()) {
confSettings.spaceKey = determineSpaceKey(page);
if ("".equals(confSettings.spaceKey) || confSettings.spaceKey == null) {
String error = "Could not find spacekeys. Note: the auto-detect spacekeys" +
" framework is being used. You must choose directories not individual files" +
" for conversion.\nCannot upload files to Confluence. Exiting.";
log.error(error);
this.errors.addError(Feedback.BAD_SPACE, error, true);
this.state.updateProgress(this.state.getMax());
this.running = false;
return "";
}
if (!createSpace(confSettings)) return null;
} //else otherwise use the default (settings based) spacekey
return sendPage(page, parentId, confSettings);
}
Pattern spacepermPattern = Pattern.compile("[{]groupname[}](.*?)[{]permissions[}](.*)");
private void updateSpacePermissions(ConfluenceServerSettings confSettings) {
if (!this.miscProperties.containsKey(PROPKEY_SPACEPERMS)) return;
String allperms = null;
String groupname = null;
Vector<String> perms = new Vector<String>();
String spaceperms = this.miscProperties.getProperty(PROPKEY_SPACEPERMS);
boolean addgroup = Boolean.parseBoolean(this.miscProperties.getProperty("spaceperms-addgroup", "true"));
Matcher permsFinder = spacepermPattern.matcher(spaceperms);
if (permsFinder.find()) {
groupname = permsFinder.group(1);
allperms = permsFinder.group(2);
String[] permsArray = allperms.split(",");
for (String perm : permsArray) {
perms.add(perm);
}
}
if (groupname != null && !perms.isEmpty()) {
RemoteWikiBroker broker = RemoteWikiBroker.getInstance();
try {
if (addgroup && !broker.hasGroup(confSettings, groupname)) {
log.info("Adding group: " + groupname);
broker.addGroup(confSettings, groupname);
}
log.debug("Updating permissions...");
broker.addPermissionsToSpace(confSettings, perms, groupname);
log.info("Updated permissions for group: " + groupname + " in space " + confSettings.getSpaceKey());
} catch (Exception e) {
String message = "Could not update permissions ('"+allperms+"') for groupname: '" + groupname +"'";
getErrors().addError(Feedback.REMOTE_API_ERROR,
message,
true);
log.error(message,e);
}
}
}
public ConfluenceServerSettings getConfluenceServerSettings(
UWCUserSettings settings) {
ConfluenceServerSettings confSettings = new ConfluenceServerSettings();
confSettings.login = settings.getLogin();
confSettings.password = settings.getPassword();
confSettings.url = settings.getUrl();
confSettings.spaceKey = settings.getSpace();
confSettings.truststore = settings.getTruststore();
confSettings.trustpass = settings.getTrustpass();
confSettings.trustallcerts = settings.getTrustall();
return confSettings;
}
/**
* creates a space for the spacekey in the given settings, if it
* doesn't already exist
* @param confSettings
* @return false if space could not be created
*/
protected boolean createSpace(ConfluenceServerSettings confSettings) {
String spaceName = confSettings.spaceKey;
String description = "This space was auto-generated by the UWC.";
return createSpace(confSettings, spaceName, description, false, null);
}
/**
* creates a space for the spacekey in the given settings, if it doesn't already exist
* @param confSettings
* @param name name of space to be created. will not be used, if space already exists
* @param description description of space to be created. will not be used if space already exists
* @param isPersonalSpace true if space is personal space
* @param personalSpaceUsername should be non-null if isPersonalSpace is true. If this condition is not
* maintained, will use spacekey in confSettings instead
* @return false if space could not be created
*/
protected boolean createSpace(ConfluenceServerSettings confSettings, String name, String description,
boolean isPersonalSpace, String personalSpaceUsername) {
String spacekey = confSettings.spaceKey;
RemoteWikiBroker broker = RemoteWikiBroker.getInstance();
SpaceForXmlRpc space = broker.createSpace(spacekey, name, description);
if (isPersonalSpace) {
space.setType(SpaceType.PERSONAL);
space.setUsername(personalSpaceUsername);
}
try {
// Conf 2.x will throw an exception if the space doesn't exist.
SpaceForXmlRpc space2 = broker.getSpace(confSettings, spacekey);
// Conf 3.x will not throw an exception but will return null
if (space2 == null) { //Confluence 3.x
String note = "Creating space with spacekey '" + spacekey + "' and name: " + name;
log.info(note);
this.state.updateNote(note);
try {
broker.addSpace(confSettings, space);
//at some point in Confluence 4.x, the Home page stopped being set to Home, so let's prestore the homepage id
Vector newspacepages = broker.getAllServerPageSummaries(confSettings, space.getSpaceKey());
PageForXmlRpc newhome = (PageForXmlRpc) newspacepages.get(0); //should only be one at this point
this.homepages.put(space.getSpaceKey(), newhome.getId());
//check to see if we're setting any permissions
updateSpacePermissions(confSettings);
} catch (Exception e) {
getErrors().addError(Feedback.BAD_LOGIN,
"Could not create space: " + spacekey +
" with login: " + confSettings.login +
". That login may not have permission to create spaces.",
true);
e.printStackTrace();
return false;
}
}
} catch (Exception e) { //Confluence 2.x
try { //exception! So, try adding the space
String note = "Creating space with spacekey '" + spacekey + "' and name: " + name;
log.info(note);
this.state.updateNote(note);
broker.addSpace(confSettings, space);
} catch (Exception e1) { //something bad happened.
getErrors().addError(Feedback.BAD_LOGIN,
"Could not create space: " + spacekey +
" with login: " + confSettings.login +
". That login may not have permission to create spaces.",
true);
e1.printStackTrace();
return false;
}
}
return true;
}
/**
* determines the correct spacekey for the given page.
* Note: used in conjunction with the autoDetectSpacekeys Framework
* @param page
* @return spacekey
*/
protected String determineSpaceKey(Page page) {
log.debug("determining space key. page = " + page.getName());
String path = page.getPath();
log.debug("determining space key. path = " + path);
String ignore = this.miscProperties.getProperty("auto-detect-ignorable-ancestors", "");
if (this.miscProperties.containsKey("filepath-hierarchy-ignorable-ancestors") &&
page.getName().equals(path)) {
ignore = ""; //turn off the ignore property - this is the top level directory
}
if (!"".equals(ignore)) {
log.debug("Ignoring these ancestors: " + ignore);
path = page.getFile().getParentFile().getAbsolutePath().
replaceFirst("^\\Q" + ignore + "\\E", "");
}
String validated = validateSpacekey(path);
log.debug("validated space key = " + validated);
return validated;
}
/**
* validates the given spacekey, removing illegal chars,
* as necessary
* @param spacekey
* @return valid spacekey
*/
public static String validateSpacekey(String spacekey) {
String validated = spacekey.replaceAll("[^A-Za-z0-9]", "");
return validated;
}
/*********
* to send a page table which has all the attributes for a page to wiki
* @param broker
* @param pageTable
* @param confSettings
* @return
*/
protected String sendPage(RemoteWikiBroker broker, Hashtable pageTable, ConfluenceServerSettings confSettings)
{
PageForXmlRpc brokerPage = PageForXmlRpc.create(pageTable);
PageForXmlRpc newPage = null;
try {
newPage = broker.storeNewOrUpdatePage(confSettings, confSettings.spaceKey, brokerPage);
if (newPage == null || newPage.getPageParams() == null) {
String message = "Unknown problem occured while sending page '" + pageTable.get("title") +
"'. See atlassian-confluence.log for more details.";
log.error(message);
getErrors().addError(Feedback.REMOTE_API_ERROR, message, true);
return null;
}
} catch (Exception e) {
getErrors().addError(Feedback.REMOTE_API_ERROR,
"The Remote API threw an exception when it tried to upload page: \"" +
pageTable.get("title") +
"\".", true);
e.printStackTrace();
return null;
}
log.debug("Page URL: " + newPage.getUrl());
//move the page if necessary and you can
log.debug("Identifying parent location for page...");
String parentid = null;
if (pageTable.containsKey("parentId"))
parentid = (String) pageTable.get("parentId");
else { //We would like to put pages, by default, under the homepage.
if (this.homepages.containsKey(confSettings.spaceKey)) {//is there a known Home page in the space?
parentid = this.homepages.get(confSettings.spaceKey);
if ("-1".equals(parentid)) parentid = null;// no homepage in this space
}
else { //can we find the home page for this space?
try {
log.debug("Identifying Homepage for spacekey: " + confSettings.spaceKey);
SpaceForXmlRpc space = broker.getSpace(confSettings, confSettings.spaceKey);
parentid = space.getSpaceParams().get("homePage");
this.homepages.put(confSettings.spaceKey, parentid);
} catch (Exception e) {
parentid = null;
this.homepages.put(confSettings.spaceKey, "-1");
}
}
}
if (parentid != null) {
log.debug("Attempting to set parent to: " + parentid);
try {
broker.movePage(confSettings, newPage.getId(), parentid, RemoteWikiBroker.Position.APPEND);
} catch (Exception e) {
log.error("Could not move page " + pageTable.get("title") + "\n" + e.getMessage() + "\n" +
e.getStackTrace()); //could be because Confluence is earlier than 2.9
}
}
return newPage.getId();
}
/*********
* to send a blog table which has all the attributes for a blog to wiki
* @param broker
* @param pageTable
* @param confSettings
* @return
*/
protected String sendBlog(RemoteWikiBroker broker, Hashtable pageTable, ConfluenceServerSettings confSettings)
{
BlogForXmlRpc brokerPage = BlogForXmlRpc.create(pageTable);
BlogForXmlRpc newPage = null;
try {
newPage = broker.storeBlog(confSettings, confSettings.spaceKey, brokerPage);
if (newPage == null || newPage.getblogParams() == null) {
String message = "Unknown problem occured while sending page '" + pageTable.get("title") +
"'. See atlassian-confluence.log for more details.";
log.error(message);
getErrors().addError(Feedback.REMOTE_API_ERROR, message, true);
return null;
}
} catch (Exception e) {
getErrors().addError(Feedback.REMOTE_API_ERROR,
"The Remote API threw an exception when it tried to upload page: \"" +
pageTable.get("title") +
"\".", true);
e.printStackTrace();
return null;
}
log.debug("Page URL: " + newPage.getUrl());
return newPage.getId();
}
protected void checkConfluenceSettings(ConfluenceServerSettings confSettings)
{
//check for problems with settings
Feedback testConnectionFeedback = TestSettingsListener.getConnectionFeedback(confSettings, isAutoDetectingSpacekeys());
if (testConnectionFeedback != Feedback.OK) {
String message = TestSettingsListener.getConnectionFeedbackMessage(confSettings, isAutoDetectingSpacekeys());
log.error(message);
this.state.updateNote(message);
throw new IllegalArgumentException(message);
}
// log.info(TestSettingsListener.SUCCESS_MESSAGE_LONG); //this is getting called for every page
}
/**
* sends page using the given settings
* @param page
* @param parentId
* @param confSettings
* @return page id the confluence page id for the page being stored. Used if
* page is new.
*/
protected String sendPage(Page page, String parentId, ConfluenceServerSettings confSettings) {
//create wiki broker
RemoteWikiBroker broker = RemoteWikiBroker.getInstance();
//update page content to be xhtml
if (Boolean.parseBoolean(this.miscProperties.getProperty("engine-markuptoxhtml", "true"))) {
page = pageContentToXhtml(broker, confSettings, page);
} else {
log.debug("Engine: markup to xhtml property set to false");
}
//create page that broker can use
Hashtable pageTable = createPageTable(page, parentId);
//check for problems with settings
checkConfluenceSettings(confSettings); //XXX Why are we doing this for every page? 'cause we seem to create the confSettings on a page by page basis?
//write ancestors, if any, first
if (page.getAncestors() != null && !page.getAncestors().isEmpty()) {
pageTable = handleAncestors(page, confSettings, pageTable);
}
//send page
String id = null;
if (!(page instanceof VersionPage) && page.getFile() != null) {
log.debug("Original Filepath: " + page.getFile().getAbsolutePath());
}
String tmpspacekey = (page.getSpacekey()!=null)?page.getSpacekey():confSettings.spaceKey;
if (page.isBlog()) {
log.debug("Attempting to send blog: " + page.getName() + " to space: " + tmpspacekey);
id = sendBlog(broker, pageTable, confSettings);
} else {
log.debug("Attempting to send page: " + page.getName() + " to space: " + tmpspacekey);
id = sendPage(broker, pageTable, confSettings);
}
if (id == null) return null;
//send attachments
sendAttachments(page, broker, id, confSettings);
//send labels
sendLabels(page, broker, id, confSettings);
//send comments
sendComments(page, broker, id, confSettings);
//set author
log.debug("Page Version: " + page.getVersion());
sendAuthor(page, broker, id, confSettings);
//set timestamp
sendTimestamp(page, broker, id, confSettings);
//return the page id
return id;
}
private Hashtable handleAncestors(Page page,
ConfluenceServerSettings confSettings, Hashtable pageTable) {
enforceAncestorTitleAndKey(page.getAncestors(), page.getName(), page.getSpacekey(), page.isBlog());
if (page.isBlog()) { //get the blog id to make certain all ancestors and current page are made the same CEO
Page first = page.getAncestors().remove(0);
String blogid = sendPage(first, null, confSettings);
enforceBlogId(page, page.getAncestors(), blogid);
pageTable.put("id", blogid);
}
if (page.getAncestors() != null) log.info("Number of ancestors for page '"+page.getName()+"': " + page.getAncestors().size());
writePages(page.getAncestors(), settings.getSpace(), false);
return pageTable;
}
private void enforceBlogId(Page page, Vector<VersionPage> pages,
String blogid) {
page.setId(blogid);
for (VersionPage anc : pages) {
anc.setId(blogid);
}
}
private void enforceAncestorTitleAndKey(Vector<VersionPage> pages,
String name, String spacekey, boolean isBlog) {
for (VersionPage page : pages) {
page.setName(name);
page.setSpacekey(spacekey);
page.setIsBlog(isBlog);
}
}
public String markupToXhtml(String markup) {
RemoteWikiBroker broker = RemoteWikiBroker.getInstance();
ConfluenceServerSettings confSettings = getConfluenceServerSettings(this.settings);
try {
return getContentAsXhtmlFormat(broker, confSettings, markup);
} catch (Exception e) {
String errorMessage = "Could not transform wiki content from markup to xhtml.";
log.error(Feedback.REMOTE_API_ERROR + ": " + errorMessage);
this.errors.addError(Feedback.REMOTE_API_ERROR, errorMessage, true);
return null;
}
}
private Page pageContentToXhtml(RemoteWikiBroker broker,
ConfluenceServerSettings confSettings, Page page) {
try {
String xhtml = getContentAsXhtmlFormat(broker, confSettings, page.getConvertedText());
page.setConvertedText(xhtml);
} catch (Exception e) {
String errorMessage = "Could not transform wiki content in page: '"+page.getName()+
"' from markup to xhtml.";
log.error(Feedback.REMOTE_API_ERROR + ": " + errorMessage);
this.errors.addError(Feedback.REMOTE_API_ERROR, errorMessage, true);
}
return page;
}
/**
* creates a parameter table with the given page and parentId.
* @param page
* @param parentId
* @return table with Remote API page parameters
*/
private Hashtable createPageTable(Page page, String parentId) {
Hashtable table = new Hashtable();
table.put("content", page.getConvertedText());
table.put("title", page.getName());
if (parentId != null && !parentId.equals("null")) table.put("parentId", parentId);
if (page.getVersion() > 0) table.put("version", page.getVersion() + "");
if (page.isBlog() && page.getId() != null) table.put("id", page.getId());
return table;
}
/**
* checks the given page for valid attachments,
* and sends them to Confluence using the rwb and the given pageId string as the
* page to be attached to.
*
* @param page given page object that might have attachments
* @param broker XML-RPC broker which will communicate with Confluence
* @param pageId the page the Confluence will attach the attachment to
* @param confSettings the confluence user settings needed by the broker to connect to Confluence
*/
private void sendAttachments(Page page, RemoteWikiBroker broker, String pageId, ConfluenceServerSettings confSettings) {
log.debug("Examining attachments for page: " + page.getName());
// Send the attachments
for (Attachment attachment : page.getAllAttachmentData()) {
if (alreadyAttached(page, attachment.getFile()))
continue;
sendAttachment(attachment, broker, pageId, confSettings);
}
}
protected void sendLabels(Page page, RemoteWikiBroker broker, String pageId, ConfluenceServerSettings confSettings) {
log.debug("Examining labels for page: " + page.getName());
//check to see if we're sending labels for this version of the page
if (badVersionForSendingLabels(page)) return;
String labels = page.getLabelsAsString();
log.debug("Sending Labels: " + labels);
if (labels == null)
return;
try {
broker.addLabels(confSettings, labels, pageId);
} catch (Exception e) {
String errorMessage = "Could not add labels '" + labels + "' to page '" + page.getName() +"'";
log.error(Feedback.REMOTE_API_ERROR + ": " + errorMessage);
this.errors.addError(Feedback.REMOTE_API_ERROR, errorMessage, true);
}
}
private boolean badVersionForSendingLabels(Page page) {
int version = page.getVersion();
int latest = Page.getLatestVersion(page.getName());
boolean history = isHandlingPageHistories();
String allVersionsProp = (String) this.miscProperties.get("page-history-allversionlabels");
boolean allVersions = (allVersionsProp != null) && Boolean.parseBoolean(allVersionsProp);
return history && !allVersions && version != latest;
}
/**
* adds page comments to a page in confluence
* @param page given page object that might have comments
* @param broker XML-RPC broker which will communicate with Confluence
* @param pageId the page id for the given page
* @param confSettings the confluence user settings needed by the broker to connect to Confluence
*/
protected void sendComments(Page page, RemoteWikiBroker broker, String pageId, ConfluenceServerSettings confSettings) {
if (page.hasComments()) {
log.debug("Sending comments for page: " + page.getName());
try {
for (Comment comment : page.getAllCommentData()) {
if (comment == null) {
log.error("Comment was null! SKIPPING");
this.errors.addError(Feedback.CONVERTER_ERROR, "Comment should not be null!", true);
continue;
}
//create page that broker can use
CommentForXmlRpc brokerComment = new CommentForXmlRpc();
brokerComment.setPageId(pageId);
String commentcontent = comment.text;
if (!comment.isXhtml()) {
commentcontent = getContentAsXhtmlFormat(broker, confSettings, comment.text);
}
brokerComment.setContent(commentcontent);
//upload comment
CommentForXmlRpc uploadedComment = broker.addComment(confSettings, brokerComment);
if (comment.hasCreator()) {
boolean usersMustExist = false;
broker.setCreator(confSettings, comment.creator, uploadedComment.getId(), usersMustExist);
broker.setLastModifier(confSettings, comment.creator, uploadedComment.getId(), usersMustExist);
}
if (comment.hasTimestamp()) {
broker.setCreateDate(confSettings, comment.timestamp, uploadedComment.getId());
broker.setLastModifiedDate(confSettings, comment.timestamp, uploadedComment.getId());
}
}
} catch (Exception e) {
String errorMessage = null;
if (e.getMessage() == null) {
log.error("Problem with comments!", e);
return;
}
else if (e.getMessage().contains("NotPermittedException")) {
errorMessage = "User is not permitted to add comments to page: " + page.getName() + "'";
}
else if (e.getMessage().contains("does not exist")) {
errorMessage = "Cannot add comments to the page because it does not exist: " + page.getName();
}
else {
errorMessage = "Could not send comments to page '" + page.getName() +"'";
}
log.error(Feedback.REMOTE_API_ERROR + ": " + errorMessage);
this.errors.addError(Feedback.REMOTE_API_ERROR, errorMessage, true);
}
}
// else log.debug("Page has no comments."); //DELETE
}
public String getContentAsXhtmlFormat(RemoteWikiBroker broker, ConfluenceServerSettings confSettings, String text) throws XmlRpcException, IOException {
return broker.convertWikiToStorageFormat(confSettings, text);
}
private void sendAuthor(Page page, RemoteWikiBroker broker, String id, ConfluenceServerSettings confSettings) {
if (page.getAuthor() != null) {
log.debug("Sending author data.");
boolean exist = true;
if (this.miscProperties.containsKey("users-must-exist"))
exist = Boolean.parseBoolean((String) this.miscProperties.get("users-must-exist"));
try {
if (page.getVersion() == 1) { //only set the creator if its the first version
broker.setCreator(confSettings, page.getAuthor(), id, exist);
}
//set the modifier
broker.setLastModifier(confSettings, page.getAuthor(), id, exist);
} catch (Exception e) {
String errorMessage = Feedback.REMOTE_API_ERROR + ": Problem setting creator or last modifier data.";
log.error(errorMessage);
this.errors.addError(Feedback.REMOTE_API_ERROR, errorMessage, true);
e.printStackTrace();
}
}
}
private void sendTimestamp(Page page, RemoteWikiBroker broker, String id, ConfluenceServerSettings confSettings) {
if (page.getTimestamp() != null) {
log.debug("Sending timestamp data: " + page.getTimestamp());
try {
DateFormat dateFormat = new SimpleDateFormat("yyyy:MM:dd:HH:mm:ss:SS"); //XXX Settable?
if (this.miscProperties.getProperty("user-timezone", null) != null) {
String timezone = this.miscProperties.getProperty("user-timezone", null);
timezone = timezone.trim();
dateFormat.setTimeZone(TimeZone.getTimeZone(timezone));
}
String timestamp = dateFormat.format(page.getTimestamp());
if (page.getVersion() == 1) { //only set the creator if its the first version
broker.setCreateDate(confSettings, timestamp, id);
}
//set the modifier
broker.setLastModifiedDate(confSettings, timestamp, id);
} catch (Exception e) {
String errorMessage = Feedback.REMOTE_API_ERROR + ": Problem setting create or last modified date.";
log.error(errorMessage);
this.errors.addError(Feedback.REMOTE_API_ERROR, errorMessage, true);
e.printStackTrace();
}
}
}
/**
* @param file
* @return true if the given file does not exist on the filesystem.
*/
protected boolean doesNotExist(File file) {
boolean doesNotExist = !file.exists();
if (doesNotExist)
log.warn("File '" + file.getPath() + "' does not exist: Skipping");
return doesNotExist;
}
/**
* @param file
* @return true if file size is too big
*/
protected boolean tooBig(File file) {
if (!file.exists()) return false;
int length = (int) file.length();
String maxString = getMaxAttachmentSizeString();
int maxBytes = getAsBytes(maxString);
if (maxBytes < 0) return false;
boolean tooBig = length > maxBytes;
if (tooBig)
log.warn("File " + file.getName() + " is larger than " + maxString + ". Skipping.");
return tooBig;
}
/**
* @return the max attachment size as a string
*/
private String getMaxAttachmentSizeString() {
return getMaxAttachmentSizeStringFromModel();
}
/**
* @return the max attachment size as it's represented in the model
*/
private String getMaxAttachmentSizeStringFromModel() {
return this.settings.getAttachmentSize();
}
/**
* @param fileLocation location of properties file
* @return a Properties object containing confluence settings
*/
protected Properties loadProperties(String fileLocation) {
/* most of this code grabbed from ConfluenceSettingsForm.populateConfluenceSettings*/
Properties properties = new Properties();
File confSettings = new File(fileLocation);
if (confSettings.exists()) {
// load properties file
FileInputStream fis;
try {
fis = new FileInputStream(fileLocation);
properties.load(fis);
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return properties;
}
/**
* calculates the number of bytes the given maxString represents
* @param maxString file size described as a String.
* Example: 5B, 5K, 5M, 5G, etc.
* @return as Bytes.
* Respectively: 5, 5120, 5242880, 5368709120
*/
protected int getAsBytes(String maxString) {
String maxRegex = "^(\\d+)(\\D)";
if (maxString == null || "".equals(maxString))
return -1;
int power, num = 0;
String numString, unitString = null;
if (Pattern.matches("^\\d+$", maxString)) {
unitString = "B";
numString = maxString;
}
else {
numString = maxString.replaceFirst(maxRegex, "$1");
unitString = maxString.replaceFirst(maxRegex, "$2");
}
try {
num = Integer.parseInt(numString);
} catch (NumberFormatException e) {
String message = PROP_ATTACHMENT_SIZE_MAX + " setting is malformed.\n" +
"Setting must be formatted like so: [number][unit], where unit is\n" +
"one of the following: B, K, M, G. No max attachment size set.";
log.error(message);
return -1;
}
unitString = unitString.toUpperCase();
char unit = unitString.toCharArray()[0]; //first char in that string
switch (unit) {
case ('B'): power = 0;break;
case ('K'): power = 1;break;
case ('M'): power = 2;break;
case ('G'): power = 3;break;
default: return -1;
}
int multiplier = (int) Math.pow(1024, power);
int value = num * multiplier;
return value;
}
/**
* @param page
* @param file
* @return true if a particular page already has a particular
* file attached.
*/
protected boolean alreadyAttached(Page page, File file) {
String pagename = page.getName();
String filename = file.getName();
boolean isblog = page.isBlog();
String attachmentId = pagename + filename + isblog;
if (page.getSpacekey() != null) attachmentId = page.getSpacekey() + attachmentId;
if (attachedFiles == null)
attachedFiles = new HashSet<String>();
boolean attached = attachedFiles.contains(attachmentId);
if (!attached) {
attachedFiles.add(attachmentId);
if (attachedPaths == null) attachedPaths = new HashSet<String>();
attachedPaths.add(file.getAbsolutePath()); //used with orphan upload checking
}
else log.debug("Attachment " + filename + " is already attached: Skipping.");
return attached;
}
/**
* to check if the file has been attached with any pages.
* @param fileName
* @return
*/
// protected boolean alreadyAttached(String fileName) {
// if (this.attachedFiles == null || this.attachedFiles.isEmpty())
// return false;
// Iterator <String>it=attachedFiles.iterator();
// while (it.hasNext())
// {
// String item=it.next();
// int index=item.lastIndexOf(fileName);
// if (index < 0)
// continue;
// if (item.length() - index == fileName.length())
// return true;
// }
// return false;
//
// }
protected boolean orphanAlreadyAttached(File file) {
if (this.attachedPaths == null) return false;
return (this.attachedPaths.contains(file.getAbsolutePath()));
}
/**
* This method determines the mime type of a file. It uses the file
* conf/mime.types to map from the file name extension
* to a mime type. The mime type file should be read into the
* mimeTypes field before this method is called.
*
* @param file The file object
* @return the mime type of the file.
*/
public static String determineContentType(File file) {
if (mimeTypes != null) {
return mimeTypes.getContentType(file);
}
//else assume it's an image
String filename = file.getName();
int extensionStart = filename.lastIndexOf(".");
if (extensionStart >= 0) {
String extension = filename.substring(extensionStart + 1);
return "image/" + extension;
}
// Hmm... No extension. Assume it's a text file.
return "text/plain";
}
Pattern switchPattern = Pattern.compile("switch");
Pattern suffixPattern = Pattern.compile("suffix");
private boolean handlingPageHistories = false;
private String pageHistorySuffix = null;
/**
* set the page history state to reflect the page history property
* and associated value that are passed as arguments
* @param key
* @param value
*/
protected void handlePageHistoryProperty(String key, String value) {
Matcher switchFinder = switchPattern.matcher(key);
if (switchFinder.find()) {
//the default should be false, so it's ok to just parse the string.
this.handlingPageHistories = Boolean.parseBoolean(value);
return;
}
Matcher suffixFinder = suffixPattern.matcher(key);
if (suffixFinder.find()) {
setPageHistorySuffix(value);
return;
}
}
protected void handleIllegalHandling(String key, String value) {
boolean enabled = false; //default, confluence 4 doesn't appear to need this
value = value.trim();
if ("false".equals(value))
enabled = false;
illegalHandlingEnabled = enabled;
}
protected void handleAutoDetectSpacekeys(String key, String value) {
boolean enabled = false; //default
value = value.trim();
if ("true".equals(value)) {
enabled = true;
}
autoDetectSpacekeys = enabled;
}
Pattern miscPropsPattern = Pattern.compile("" +
"\\w+\\.\\d+\\.([^.]+)\\.property"
);
protected Properties handleMiscellaneousProperties(String key, String value) {
Matcher miscKeyFinder = miscPropsPattern.matcher(key);
if (miscKeyFinder.matches()) {
String misckey = miscKeyFinder.group(1);
if (this.miscProperties == null)
this.miscProperties = new Properties();
this.miscProperties.put(misckey, value);
log.debug("Miscellaneous Property set: " + misckey + "=" + value);
return this.miscProperties;
}
String error = "Miscellaneous property was detected, " +
"but key was invalid. Could not instantiate property: " +
key + "=" + value;
log.error(error);
this.errors.addError(Feedback.BAD_PROPERTY, error, true);
return this.miscProperties;
}
private void addDefaultMiscProperties() {
handleMiscellaneousProperties("Testing.1234.spacekey.property", this.settings.getSpace());
}
protected void handleFilters(String key, String value) throws InstantiationException, IllegalAccessException {
log.debug("filter property = " + value);
getFilterValues().add(value);
}
private Set<String> getFilterValues() {
if (this.filterValues == null)
this.filterValues = new HashSet<String>();
return this.filterValues;
}
/**
* sets up .xmlevent properties
* @param key must end in .xmlevent
* @param value must follow this format:
* <tt>
* {tag}tagname{class}classname
* </tt>
* where tagname is the xml tag to associate the event with (b, for bold)
* and classname is the parser that will manage the events for that tag.
* tagname can contain a comma-delimited list of tags. For example:
* {tag}h1, h2, h3{class}com.example.HeaderParser
*/
private void handleXmlEvents(String key, String value) {
String tag = getXmlEventTag(value);
String classname = getXmlEventClassname(value);
String[] tags = tag.split(",");
for (String onetag : tags) {
onetag = onetag.trim();
addOneXmlEvent(onetag, classname);
}
}
/**
* adds one xml event object to the events handler, such that the classname becomes
* an instantiated class that is associated with the given tag.
* The events handler can be custom (using the xmlevents misc property), or the default
* xml events handler will be used
* @param tag
* @param classname
*/
private void addOneXmlEvent(String tag, String classname) {
if (this.miscProperties.containsKey("xmlevents")) {
Class eventsClass;
String xmleventsclass = this.miscProperties.getProperty("xmlevents");
try {
eventsClass = Class.forName(xmleventsclass);
} catch (ClassNotFoundException e) {
log.warn("xmlevents property value - " +
xmleventsclass +
" - does not exist. Using DefaultXmlEvents.");
this.miscProperties.remove("xmlevents");
eventsClass = DefaultXmlEvents.class; //try setting the DefaultXmlEvents
}
XmlEvents events = null;
try {
events = (XmlEvents) eventsClass.newInstance();
events.addEvent(tag, classname); //call the custom events class
return;
} catch (Exception e) {
log.warn("xmlevents property value - " +
xmleventsclass + " - hasn't implemented XmlEvents. " +
"Using DefaultXmlEvents.");
this.miscProperties.remove("xmlevents");
//continue to DefaultXmlEvents.addEvent below
}
}
new DefaultXmlEvents().addEvent(tag, classname);
}
Pattern xmleventClassPattern = Pattern.compile("" +
"\\{class\\}(.*)");
protected String getXmlEventClassname(String value) {
Matcher finder = xmleventClassPattern.matcher(value);
if (finder.find()) {
return finder.group(1);
}
throw new IllegalArgumentException(XMLEVENT_PROP_ERROR);
}
Pattern xmleventTagPattern = Pattern.compile("" +
"\\{tag\\}([^}]+)\\{class\\}");
protected String getXmlEventTag(String value) {
Matcher finder = xmleventTagPattern.matcher(value);
if (finder.find()) {
return finder.group(1);
}
throw new IllegalArgumentException(XMLEVENT_PROP_ERROR);
}
/**
* @param key
* @return true if the given key is the switch to turn on the
* Hierarchy framework
*/
protected boolean isHierarchySwitch(String key) {
Matcher switchFinder = switchPattern.matcher(key);
return switchFinder.find();
}
/**
* determines if the given string represents an allowed
* non converter property: (hierarchy builder, page history preserver,
* illegalname handler, autodetect spacekeys)
* @param input represents an entire converter/property string. For example:
* <br/>
* Wiki.0011.somefilename.propertytype=something
* @return true if it's an expected/allowed non converter property
*/
public boolean isNonConverterProperty(String input) {
String converterTypes =
"(" +
"(" +
NONCONVERTERTYPE_HIERARCHYBUILDER +
")" +
"|" +
"(" +
NONCONVERTERTYPE_PAGEHISTORYPRESERVATION +
")" +
"|" +
"(" +
NONCONVERTERTYPE_ILLEGALHANDLING +
")" +
"|" +
"(" +
NONCONVERTERTYPE_AUTODETECTSPACEKEYS +
")" +
"|" +
"(" +
NONCONVERTERTYPE_FILTERS +
")" +
"|" +
"(" +
NONCONVERTERTYPE_MISCPROPERTIES +
")" +
"|" +
"(" +
NONCONVERTERTYPE_XMLEVENT +
")" +
")";
String converterPattern = "[-\\w\\d.]+?" + converterTypes + "=" + ".*";
return input.matches(converterPattern);
}
/**
* @return true if the converter should handle page histories
*/
public boolean isHandlingPageHistories() {
return this.handlingPageHistories;
}
/**
* @return true if the converter should handle page histories
*/
public boolean isHandlingPageHistoriesFromFilename() {
return this.handlingPageHistories && this.pageHistorySuffix != null;
}
/**
* @return the current page history suffix
*/
public String getPageHistorySuffix() {
return this.pageHistorySuffix;
}
/**
* sorts the given list of pages.
* Note: sorting will take into account page name
* and page version. Non unique page objects will be culled.
* @param pages list of Page objects
* @return sorted list
*/
protected List<Page> sortByHistory(List<Page> pages) {
this.state.updateNote("Sorting Pages by Page History");
log.debug("num of pages: " + pages.size());
List<Page> sortedPages = new ArrayList<Page>();
Set<Page> sorted = new TreeSet<Page>();
sorted.addAll(pages); //sort them and get rid of non-unique pages
sortedPages.addAll(sorted); //turn them back into a list
if (log.isDebugEnabled()) {
for (Page page : sorted) {
log.debug("Sorted pages: " + page.getFile().getName());
}
}
return sortedPages;
}
Pattern hashPattern = Pattern.compile("#+");
/**
* sets the page history suffix, if it's a valid suffix.
* If not, sets it to null.
* @param suffix candidate suffix, a valid candidate will have
* a numeric component, represented by a '#' (hash) symbol
* <br/>
* Example: -v#.txt
* @return true, if a valid suffix was saved.
* false, if the suffix was invalid, and therefore was not saved.
*/
protected boolean setPageHistorySuffix(String suffix) {
//check for suffix goodness
Matcher hashFinder = hashPattern.matcher(suffix);
if (hashFinder.find()) {
this.pageHistorySuffix = suffix;
return true;
}
log.error("Error trying to preserve page history: Suffix '" + suffix + "' " +
"does not have a sortable component. Must include '#'.");
this.pageHistorySuffix = null;
return false;
}
/**
* @return HierarchyBuilder object. used by tests.
*/
protected HierarchyBuilder getHierarchyBuilder() {
return hierarchyBuilder;
}
/**
* @return HierarchyHandler object. used by tests.
*/
protected HierarchyHandler getHierarchyHandler() {
return hierarchyHandler;
}
/**
* sets how the hierarchy framework is to be used.
* @param input "UseBuilder", "UsePagenames", or "Default".
* If input is none of these, no changes occur
*/
private void setHierarchyHandler(String input) {
if (input.matches("UseBuilder")) hierarchyHandler = HierarchyHandler.HIERARCHY_BUILDER;
else if (input.matches("UsePagenames")) hierarchyHandler = HierarchyHandler.PAGENAME_HIERARCHIES;
else if (input.matches("Default")) hierarchyHandler = HierarchyHandler.DEFAULT;
}
/**
* @return the feedback as it currently stands
*/
public Feedback getConverterFeedback() {
return this.feedback;
}
/**
* resets the feedback state to Feedback.NONE
*/
public void resetFeedback() {
this.feedback = Feedback.NONE;
}
/**
* clears state relating to the error handling
*/
public void resetErrorHandlers() {
this.errors.clear();
this.hadConverterErrors = false;
}
/**
* clears state relating to the hierarchy framework
*/
public void resetHierarchy() {
this.hierarchyBuilder = null;
this.hierarchyHandler = HierarchyHandler.DEFAULT;
}
/**
* @return object contains information relating to errors triggered during the conversion
*/
public ConverterErrors getErrors() {
return this.errors;
}
/**
* @return true if the conversion has generated errors
*/
public boolean hadConverterErrors() {
return this.hadConverterErrors;
}
/**
* setter
* @param running
*/
protected void setRunning(boolean running) {
this.running = running; //used in junit
}
/**
* setter
* @param settings
*/
protected void setSettings(UWCUserSettings settings) {
this.settings = settings; //used in junit
}
/**
* @return true if the illegal handling (names and links) should occur.
* false if it should be disabled
*/
public boolean isIllegalHandlingEnabled() {
return illegalHandlingEnabled;
}
public boolean isAutoDetectingSpacekeys() {
return autoDetectSpacekeys;
}
public class AsciiVersionComparator implements Comparator {
public int compare(Object a, Object b) {
Page pa = (Page) a;
Page pb = (Page) b;
String sa = pa.getName().toLowerCase();
String sb = pb.getName().toLowerCase();
int ascii = sa.compareTo(sb);
int sav = pa.getVersion();
int sbv = pb.getVersion();
int version = sbv - sav;
return ascii - version;
}
}
}