package fitnesse.wiki.fs; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.*; import org.apache.commons.lang.StringUtils; import fitnesse.wiki.*; import fitnesse.wikitext.parser.*; import util.FileUtil; import static fitnesse.util.StringUtils.isBlank; import static java.lang.String.format; /** * With this page all content is saved in one file: WikiPageName.wiki. * Sub wiki's are stored as WikiPageMame/SubWiki.wiki. * This format should eventually replace the {@link FileSystemPage}. */ public class WikiFilePage extends BaseWikitextPage implements FileBasedWikiPage { public static final String FILE_EXTENSION = ".wiki"; public static final String ROOT_FILE_NAME = "_root" + FILE_EXTENSION; private static final SymbolProvider WIKI_FILE_PARSING_PROVIDER = new SymbolProvider( new SymbolType[] { FrontMatter.symbolType, SymbolType.Text}); private final File path; private final VersionsController versionsController; private final SubWikiPageFactory subWikiPageFactory; private final String versionName; private PageData pageData; protected WikiFilePage(final File path, final String name, final WikiPage parent, final String versionName, final VersionsController versionsController, final SubWikiPageFactory subWikiPageFactory, final VariableSource variableSource) { super(name, parent, variableSource); this.path = path; this.versionsController = versionsController; this.subWikiPageFactory = subWikiPageFactory; this.versionName = versionName; } private WikiFilePage(WikiFilePage page, String versionName) { this(page.path, page.getName(), (page.isRoot() ? null : page.getParent()), versionName, page.versionsController, page.subWikiPageFactory, page.getVariableSource()); } @Override public WikiPage addChildPage(final String childName) { return new WikiFilePage(new File(getFileSystemPath(), childName + FILE_EXTENSION), childName, this, null, this.versionsController, this.subWikiPageFactory, this.getVariableSource()); } private File getSubWikiFolder() { return path; } @Override public WikiPage getChildPage(final String childName) { return subWikiPageFactory.getChildPage(this, childName); } @Override public void removeChildPage(final String name) { final WikiPage childPage = getChildPage(name); if (childPage != null) { childPage.remove(); } } @Override public void remove() { try { versionsController.delete(path, getFileSystemPath()); } catch (IOException e) { throw new WikiPageLoadException(format("Could not remove page %s", new WikiPagePath(this).toString()), e); } } @Override public List<WikiPage> getChildren() { return subWikiPageFactory.getChildren(this); } @Override public PageData getData() { if (pageData == null) { try { pageData = getDataVersion(); } catch (IOException e) { throw new WikiPageLoadException("Could not load page data for page " + path.getPath(), e); } } return new PageData(pageData); } @Override public Collection<VersionInfo> getVersions() { return versionsController.history(path); } @Override public WikiPage getVersion(final String versionName) { try { versionsController.getRevisionData(versionName, path); } catch (IOException e) { throw new WikiPageLoadException(format("Could not load version %s for page at %s", versionName, path.getPath()), e); } return new WikiFilePage(this, versionName); } @Override public VersionInfo commit(final PageData data) { resetCache(); try { return versionsController.makeVersion(new WikiFilePageVersion(data)); } catch (IOException e) { throw new WikiPageLoadException(e); } } @Override public File getFileSystemPath() { if (ROOT_FILE_NAME.equals(path.getName())) { return path.getParentFile(); } else { String pathStr = this.path.getPath(); return new File(pathStr.substring(0, pathStr.length() - FILE_EXTENSION.length())); } } @Override protected void resetCache() { super.resetCache(); pageData = null; } private PageData getDataVersion() throws IOException { FileVersion[] versions = versionsController.getRevisionData(versionName, path); FileVersion fileVersion = versions[0]; String content = ""; WikiPageProperty properties = defaultPageProperties(); if (fileVersion != null) { try { String fileContent = loadContent(fileVersion); final ParsingPage parsingPage = makeParsingPage(this); final Symbol syntaxTree = Parser.make(parsingPage, fileContent, WIKI_FILE_PARSING_PROVIDER).parse(); if (!syntaxTree.getChildren().isEmpty()) { final Symbol maybeFrontMatter = syntaxTree.getChildren().get(0); if (maybeFrontMatter.isType(FrontMatter.symbolType)) { properties = mergeWikiPageProperties(properties, maybeFrontMatter); if (syntaxTree.getChildren().size() > 1) { content = fileContent.substring(maybeFrontMatter.getEndOffset()); } } else { content = fileContent; } } properties.setLastModificationTime(fileVersion.getLastModificationTime()); } catch (IOException e) { throw new WikiPageLoadException(e); } } pageData = new PageData(content, properties); return pageData; } private WikiPageProperty mergeWikiPageProperties(final WikiPageProperty properties, final Symbol frontMatter) { for (Symbol keyValue : frontMatter.getChildren()) { if (keyValue.isType(FrontMatter.keyValueSymbolType)) { String key = keyValue.getChildren().get(0).getContent(); String value = keyValue.getChildren().get(1).getContent(); if (isBooleanProperty(key)) { if (isBlank(value) || isTruthy(value)) { properties.set(key, value); } else if (isFalsy(value)) { properties.remove(key); } } else { WikiPageProperty symLinks = properties.set(key, value); for (int i = 2; i < keyValue.getChildren().size(); i++) { final Symbol subProperty = keyValue.getChildren().get(i); assert subProperty.isType(FrontMatter.keyValueSymbolType); String linkName = subProperty.getChildren().get(0).getContent(); String linkPath = subProperty.getChildren().get(1).getContent(); symLinks.set(linkName, linkPath); } } } } return properties; } private boolean isBooleanProperty(final String key) { return qualifiesAs(key, PageData.PAGE_TYPE_ATTRIBUTES) || qualifiesAs(key, PageData.NON_SECURITY_ATTRIBUTES) || qualifiesAs(key, PageData.SECURITY_ATTRIBUTES); } private boolean isTruthy(final String value) { return qualifiesAs(value.toLowerCase(), new String[]{"y", "yes", "t", "true", "1"}); } private boolean isFalsy(final String value) { return qualifiesAs(value.toLowerCase(), new String[]{"n", "no", "f", "false", "0"}); } private boolean qualifiesAs(final String value, final String[] qualifiers) { for (String q : qualifiers) { if (q.equals(value)) return true; } return false; } private String loadContent(final FileVersion fileVersion) throws IOException { try (InputStream content = fileVersion.getContent()) { return FileUtil.toString(content); } } private String propertiesYaml(final WikiPageProperty pageProperties) { final WikiPageProperty defaultProperties = defaultPageProperties(); final List<String> lines = new ArrayList<>(); for (String key : pageProperties.keySet()) { if (isBooleanProperty(key)) { if (!defaultProperties.has(key)) { lines.add(key); } } else if (!WikiPageProperty.LAST_MODIFIED.equals(key)) { final StringBuilder builder = new StringBuilder(); builder.append(key); if (!isBlank(pageProperties.get(key))) { builder.append(": ").append(pageProperties.get(key)); } final WikiPageProperty subProperty = pageProperties.getProperty(key); for (String pageName: subProperty.keySet()) { builder.append("\n ").append(pageName).append(": ").append(subProperty.get(pageName)); } lines.add(builder.toString()); } } for (String key : defaultProperties.keySet()) { if (isBooleanProperty(key) && !pageProperties.has(key)) { lines.add(key + ": no"); } } Collections.sort(lines); return lines.isEmpty() ? "" : "---\n" + StringUtils.join(lines, '\n') + "\n---\n"; } private class WikiFilePageVersion implements FileVersion { private final PageData data; public WikiFilePageVersion(final PageData data) { this.data = new PageData(data); } @Override public File getFile() { return path; } @Override public InputStream getContent() throws IOException { String yaml = propertiesYaml(data.getProperties()); final String content = yaml + data.getContent(); return new ByteArrayInputStream(content.getBytes(FileUtil.CHARENCODING)); } @Override public String getAuthor() { return data.getAttribute(WikiPageProperty.LAST_MODIFYING_USER); } @Override public Date getLastModificationTime() { return data.getProperties().getLastModificationTime(); } } }