Implemented extended attributes on File issues.

* Created a PEG parser for issue files.
* Added parsing and formatting code to `FileIssue` to handle extended
  properties.
This commit is contained in:
Jonathan Bernard 2011-11-21 00:46:24 -06:00
parent 66b68160e5
commit 447e74f956
5 changed files with 168 additions and 75 deletions

View File

@ -0,0 +1,13 @@
import com.jdbernard.pit.*
import com.jdbernard.pit.file.*
import org.parboiled.Parboiled
import org.parboiled.parserunners.ReportingParseRunner
import org.parboiled.parserunners.RecoveringParseRunner
parser = Parboiled.createParser(IssuePegParser.class)
parseRunner = new ReportingParseRunner(parser.IssueFile())
issueFile = new File('/Volumes/NO NAME/Dropbox/tasks/0015tn3.rst')
issueText = issueFile.text
result = parseRunner.run(issueText)
issueMap = result.valueStack.pop()

View File

@ -11,7 +11,7 @@ public abstract class Issue {
protected String text
protected String title
protected Map extendedPropeties = [:]
Map extendedProperties = [:]
Issue(Map props) {
this.id = props.id

View File

@ -0,0 +1,45 @@
package com.jdbernard.pit.file
import org.joda.time.DateMidnight
import org.joda.time.DateTime
public enum ExtendedPropertyHelp {
DATE(/^\d{4}-\d{2}-\d{2}$/, DateMidnight,
{ v -> DateMidnight.parse(v) },
{ d -> d.toString("YYYY-MM-dd") }),
DATETIME(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/, DateTime,
{ v -> DateTime.parse(v) },
{ d -> d.toString("YYYY-MM-dd'T'HH:mm:ss") }),
INTEGER(/^\d+$/, Integer,
{ v -> v as Integer },
{ i -> i as String });
String pattern;
Class klass;
def parseFun, formatFun;
public ExtendedPropertyHelp(String pattern, Class klass, def parseFun,
def formatFun) {
this.pattern = pattern
this.klass = klass
this.parseFun = parseFun
this.formatFun = formatFun }
public boolean matches(String prop) { return prop ==~ pattern }
public static Object parse(String value) {
def result = null
ExtendedPropertyHelp.values().each { propType ->
if (propType.matches(value)) { result = propType.parseFun(value) }}
return result ?: value }
public static String format(def object) {
def result = null
ExtendedPropertyHelp.values().each { propType ->
if (!result && propType.klass.isInstance(object)) {
result = propType.formatFun(object) }}
return result ?: object.toString() }
}

View File

@ -4,8 +4,8 @@ import com.jdbernard.pit.*
import java.lang.IllegalArgumentException as IAE
import org.joda.time.DateMidnight
import org.joda.time.DateTime
import org.parboiled.Parboiled
import org.parboiled.parserunners.ReportingParseRunner
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@ -17,6 +17,12 @@ public class FileIssue extends Issue {
public static final String fileExp = /(\d+)([bft])([ajnsv])(\d).*/
protected static parseRunner
static {
def parser = Parboiled.createParser(IssuePegParser)
parseRunner = new ReportingParseRunner(parser.IssueFile()) }
public FileIssue(File file) {
super(id: -1, title: 'REPLACE_ME')
@ -30,46 +36,28 @@ public class FileIssue extends Issue {
"is not a valid Issue file.")
// Read issue attributes from the filename.
super.@id = matcher[0][1]
super.@category = Category.toCategory(matcher[0][2])
super.@status = Status.toStatus(matcher[0][3])
super.@priority = matcher[0][4].toInteger()
super.id = matcher[0][1]
super.category = Category.toCategory(matcher[0][2])
super.status = Status.toStatus(matcher[0][3])
super.priority = matcher[0][4].toInteger()
log.debug("id: {}\tcategory: {}\tstatus: {}\tpriority: {}",
[super.@id, super.@category, super.@status, super.@priority])
super.id, super.category, super.status, super.priority)
this.source = file
String text = ""
boolean parsingText = true
// Parse the file and extract the title, text, and extended properties
// TODO: guard against parsing problems (null/empty value stack, etc.)
def parsedIssue = parseRunner.run(file.text).valueStack.pop()
// Now parse the actual file contents.
file.text.eachLine { line, lineNumber ->
super.text = parsedIssue.body
super.title = parsedIssue.title
log.trace("lineNumber: {}, line: {}", lineNumber, line)
if (lineNumber == 0) {
super.@title = line
log.debug("Found title: {}", super.@title) }
else if (lineNumber > 2) {
// We see the horizontal rule
if (line ==~ /\s*^\-{4}\-*\s*$/) { parsingText = false }
if (parsingText) { text += "$line\n" }
else {
def match = (line =~ /^([^:]+):(.+)$/)
if (match) {
def key = match[0][1].trim()
def value = parseValue(match[0][2].trim())
super.@extendedProperties[key] = value
}
}
}
}
super.@text = text
// Add the extended properties
parsedIssue.extProperties.each { key, value ->
key = key.toLowerCase().replaceAll(/\s/, '_')
super.extendedProperties[key] =
ExtendedPropertyHelp.parse(value) }
}
public void setCategory(Category c) throws IOException {
@ -83,8 +71,7 @@ public class FileIssue extends Issue {
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).")
else super.setCategory(c)
}
else super.setCategory(c) }
public void setStatus(Status s) throws IOException {
boolean renamed
@ -97,8 +84,7 @@ public class FileIssue extends Issue {
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).")
else super.setStatus(s)
}
else super.setStatus(s) }
public void setPriority(int p) throws IOException {
boolean renamed
@ -111,28 +97,23 @@ public class FileIssue extends Issue {
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).")
else super.setPriority(p)
}
else super.setPriority(p) }
public String getFilename() {
return makeFilename(id, category, status, priority)
}
return makeFilename(id, category, status, priority) }
public void setTitle(String title) throws IOException {
super.setTitle(title)
writeFile()
}
writeFile() }
public void setText(String text) throws IOException {
super.setText(text)
writeFile()
}
writeFile() }
boolean deleteFile() { return source.deleteDir() }
public static boolean isValidFilename(String name) {
return name ==~ fileExp
}
return name ==~ fileExp }
public static String makeFilename(String id, Category category,
Status status, int priority) {
@ -148,27 +129,28 @@ public class FileIssue extends Issue {
if (!(id ==~ /\d+/))
throw new IAE( "'${id}' is not a legal value for id.")
return id + category.symbol + status.symbol + priority + ".rst";
}
return id + category.symbol + status.symbol + priority + ".rst" }
public static String formatIssue(Issue issue) {
def result = new StringBuilder()
result.append(issue.@title)
result.append(issue.title)
result.append("\n")
result.append("=".multiply(issue.@title.length()))
result.append("\n")
result.append(issue.@text)
result.append("\n----\n")
result.append("=".multiply(issue.title.length()))
result.append("\n\n")
result.append(issue.text)
result.append("\n----\n\n")
// If there are any extended properties, let's write those.
if (issue.@extendedProperties.size() > 0) {
if (issue.extendedProperties.size() > 0) {
def extOutput = [:]
def maxKeyLen = 0
def maxValLen = 0
// Find the longest key and value, convert all to strings.
issue.@extendedProperties.each { key, val ->
def ks = key.toString(), vs = formatProperty(val)
issue.extendedProperties.each { key, val ->
def ks = key.toString().split('_').collect({it.capitalize()}).join(' ')
def vs = ExtendedPropertyHelp.format(val)
extOutput[ks] = vs
if (ks.length() > maxKeyLen) { maxKeyLen = ks.length() }
if (vs.length() > maxKeyLen) { maxValLen = vs.length() } }
@ -194,20 +176,6 @@ public class FileIssue extends Issue {
catch (IOException ioe) {
throw new IOException("I could not save the new text for this "
+ "issue. I can not write to the file for this issue. I do not"
+ " know why, I am sorry (maybe the file can not be reached).")
}
}
+ " know why, I am sorry (maybe the file can not be reached).") } }
public def parseValue(String value) {
switch (value) {
case ~/\d{4}-\d{2}-\d{2}/: return DateMidnight.parse(value)
default: return value
}
}
public String formatProperty(DateTime prop) {
return prop.format("YYYY-MM-dd'T'HH:mm:ss") }
public String formatProperty(DateMidnight prop) {
return prop.format("YYYY-MM-dd") }
}

View File

@ -0,0 +1,67 @@
package com.jdbernard.pit.file;
import java.util.HashMap;
import java.util.Map;
import org.parboiled.Action;
import org.parboiled.BaseParser;
import org.parboiled.Context;
import org.parboiled.Rule;
import org.parboiled.annotations.*;
@BuildParseTree
public class IssuePegParser extends BaseParser<Object> {
public Rule IssueFile() {
return Sequence(push(makeNode()),
Title(), Body(), Optional(PropertyBlock())); }
Rule Title() {
return Sequence(
OneOrMore(NOT_EOL), addToNode("title", match()), EOL,
HorizontalRule(), EOL,
ZeroOrMore(SPACE), EOL); }
Rule Body() { return Sequence(OneOrMore(Sequence(
TestNot(PropertyBlock()), ANY)), addToNode("body", match())); }
Rule PropertyBlock() {
return Sequence(push(makeNode()),
HorizontalRule(), OneOrMore(EOL), TableSeparator(), EOL,
OneOrMore(PropertyDefinition()), TableSeparator(),
addToNode("extProperties", pop())); }
Rule PropertyDefinition() {
return Sequence(
PropertyKey(), push(match()), COLON,
PropertyValue(), push(match()), EOL,
swap(), addToNode(popAsString().trim(), popAsString().trim())); }
Rule PropertyKey() { return OneOrMore(Sequence(TestNot(COLON), NOT_EOL)); }
Rule PropertyValue() { return OneOrMore(NOT_EOL); }
Rule TableSeparator() {
return Sequence(OneOrMore(SEPARATOR_CHAR), OneOrMore(SPACE),
OneOrMore(SEPARATOR_CHAR)); }
Rule HorizontalRule() {
return Sequence(SEPARATOR_CHAR, SEPARATOR_CHAR, SEPARATOR_CHAR,
OneOrMore(SEPARATOR_CHAR)); }
Rule EOL = Ch('\n');
Rule NOT_EOL = Sequence(TestNot(EOL), ANY);
Rule SEPARATOR_CHAR = AnyOf("\"'`~:-_=+*^#<>");
Rule SPACE = AnyOf(" \t");
Rule COLON = Ch(':');
Map makeNode() { return new HashMap(); }
boolean addToNode(Object key, Object val) {
Map node = (Map) pop();
node.put(key, val);
push(node);
return true; }
String popAsString() { return (String) pop(); }
}