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:
parent
66b68160e5
commit
447e74f956
13
libpit/resources/main/test.groovy
Normal file
13
libpit/resources/main/test.groovy
Normal 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()
|
@ -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
|
||||
|
@ -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() }
|
||||
}
|
@ -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") }
|
||||
}
|
||||
|
67
libpit/src/main/com/jdbernard/pit/file/IssuePegParser.java
Normal file
67
libpit/src/main/com/jdbernard/pit/file/IssuePegParser.java
Normal 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(); }
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user