package plume;
import static plume.EntryReader.Entry;
import java.io.*;
import java.util.*;
import java.util.regex.*;
import java.text.*;
import com.sun.javadoc.*;
/**
* TaskManager extracts information about tasks from text files and
* provides structured output. For example, it can extract all of
* the tasks associated with a specific milestone or person and total
* the amount of work required. <p>
*
* The command-line arguments are as follows:
* <!-- start options doc (DO NOT EDIT BY HAND) -->
* <ul>
* <li><b>-r</b> <b>--responsible=</b><i>string</i>. Include only those tasks assigned to the specified person</li>
* <li><b>-m</b> <b>--milestone=</b><i>string</i>. Include only those tasks required for the specified milestone</li>
* <li><b>-c</b> <b>--completed=</b><i>boolean</i>. Include only completed tasks [default false]</li>
* <li><b>-o</b> <b>--open=</b><i>boolean</i>. Include only open tasks [default false]</li>
* <li><b>-v</b> <b>--verbose=</b><i>boolean</i>. Print progress information [default false]</li>
* <li><b>-f</b> <b>--format=</b><i>enum</i>. Specify output format (short_ascii, short_html, milestone_html) [default short_ascii]</li>
* <li><b>--comment-re=</b><i>string</i>. Regex that matches an entire comment (not just a comment start) [default ^%.*]</li>
* <li><b>--include-re=</b><i>string</i>. Regex that matches an include directive; group 1 is the file name [default \\include\{(.*)\}]</li>
* </ul>
* <!-- end options doc -->
*/
public class TaskManager {
/** The format in which to output the TaskManager information. */
public enum OutputFormat {short_ascii, short_html, milestone_html};
// Command line options
@Option ("-r Include only those tasks assigned to the specified person")
public static /*@Nullable*/ String responsible = null;
@Option ("-m Include only those tasks required for the specified milestone")
public static /*@Nullable*/ String milestone = null;
@Option ("-c Include only completed tasks")
public static boolean completed = false;
@Option ("-o Include only open tasks")
public static boolean open = false;
@Option ("-v Print progress information")
public static boolean verbose = false;
@Option ("-f Specify output format (short_ascii, short_html, milestone_html)")
public static OutputFormat format = OutputFormat.short_ascii;
@Option ("Regex that matches an entire comment (not just a comment start)")
public static String comment_re = "^%.*";
@Option ("Regex that matches an include directive; group 1 is the file name")
public static String include_re = "\\\\include\\{(.*)\\}";
private static String usage_string
= "TaskManger [options] <task-file> <task_file> ...";
@SuppressWarnings("nullness") // line.separator property always exists
public static final String lineSep = System.getProperty("line.separator");
/** Information about a single task **/
public static class Task {
String filename;
long line_number;
String task;
String responsible;
/*@Nullable*/ Date assigned_date;
/*@Nullable*/ String milestone;
Float duration;
Float completed;
/*@Nullable*/ String description;
/*@Nullable*/ String notes;
private void checkRep() {
assert filename != null : "No filename at line " + line_number;
assert task != null : "No task at line " + line_number;
assert responsible != null : "No responsible at line " + line_number;
assert duration != null : "No duration at line " + line_number;
assert completed != null : "No completed at line " + line_number;
}
public Task (String body, String filename, long line_number)
throws IOException {
this.filename = filename;
this.line_number = line_number;
String[] lines = body.split (lineSep);
for (int ii = 0; ii < lines.length; ii++) {
String line = lines[ii];
// Get the item/value out of the record. One line items
// are specifed as '{item}: {value}'. Multiple line items
// have a start line of '{item}>' and an end line of '<{item}'
// with any number of value lines between.
/*@NonNull*/ String item;
String value;
if (line.matches ("^[_a-zA-Z]+:.*")) {
String[] sa = line.split (" *: *", 2);
item = sa[0];
value = sa[1];
if (value.length() == 0)
value = null;
} else if (line.matches ("^[-a-zA-Z]+>.*")) {
item = line.replaceFirst (" *>.*", "");
value = "";
for (ii++; ii < lines.length; ii++) {
String nline = lines[ii];
if (nline.equals ("<" + item))
break;
value += nline + lineSep;
}
} else {
throw new IOException ("malformed line: " + line);
}
// parse the value based on the item and store it away
if (item.equals ("task")) {
if (value == null)
throw new Error("Task with no value at line " + line_number);
task = value;
} else if (item.equals ("responsible")) {
if (value == null)
responsible = "none";
else
responsible = value;
} else if (item.equals ("assigned_date")) {
if (value == null)
assigned_date = null;
else {
DateFormat df = new SimpleDateFormat("yy-MM-dd");
try {
assigned_date = df.parse (value);
assert assigned_date != null : value;
} catch (Throwable t) {
throw new RuntimeException (t);
}
}
} else if (item.equals ("milestone")) {
if (value == null)
throw new Error("Milestone with no value at line " + line_number);
milestone = value;
} else if (item.equals ("duration")) {
if (value == null)
// duration is often used without being checked against null
throw new Error("Duration with no value at line " + line_number);
duration = Float.parseFloat (value);
} else if (item.equals ("completed")) {
if (value == null)
throw new Error("Completed with no value at line " + line_number);
completed = Float.parseFloat (value);
} else if (item.equals("description")) {
if (value == null)
throw new Error("Description with no value at line " + line_number);
description = value;
} else if (item.equals("notes")) {
if (value == null)
throw new Error("Notes with no value at line " + line_number);
notes = value;
} else {
throw new IOException ("unknown field " + item);
}
}
// Check that all required fields are set.
checkRep();
}
public static String short_str (float f) {
if (((double)f) - Math.floor ((double)(f)) > 0.1)
return String.format ("%.1f", f);
else
return String.format ("%d", Math.round (f));
}
private String completion_str() {
return String.format ("%s/%s", short_str (completed),
short_str(duration));
}
public String toString_short_ascii() {
return String.format ("%-10s %-10s %-6s %s", responsible, milestone,
completion_str(), task);
}
public String toString_short_html(double total) {
return String.format ("<tr> <td> %s </td><td> %s </td><td> "
+ "%s </td><td> %f </td><td> %s </td></tr>",
responsible, milestone, completion_str(), total, task);
}
public String toString_milestone_html(double total) {
String resp_str = responsible;
if (resp_str.equals ("none"))
resp_str = "<font color=red><b>" + resp_str + "</b></font>";
return String.format ("<tr> <td> %s </td><td> %s </td><td> %.1f </td><td>"
+ "<a href=%s?file=%s&line=%d> %s </a></td></tr>",
resp_str, completion_str(), total,
"show_task_details.php",
filename, line_number, task);
}
public String all_vals() {
StringBuilder out = new StringBuilder();
out.append ("task: " + task + lineSep);
out.append ("responsible: " + responsible + lineSep);
out.append ("assigned_date: " + assigned_date + lineSep);
out.append ("milestone: " + milestone + lineSep);
out.append ("duration: " + duration + lineSep);
out.append ("completed: " + completed + lineSep);
out.append ("description: " + description + lineSep);
out.append ("notes: " + notes + lineSep);
return out.toString();
}
}
/** List of all of the tasks **/
public List<Task> tasks = new ArrayList<Task>();
/** empty TaskManger **/
public TaskManager() {
}
/** initializes a task manager with all of the tasks in filenames **/
public TaskManager (String[] filenames) throws IOException {
// Read in each specified task file
for (String filename : filenames) {
filename = UtilMDE.expandFilename (filename);
EntryReader reader = new EntryReader (filename, comment_re, include_re);
while (true) {
EntryReader.Entry entry = reader.get_entry();
if (entry == null) break;
try {
tasks.add (new Task (entry.body, entry.filename, entry.line_number));
} catch (IOException e) {
throw new Error ("Error parsing " + entry.filename + " at line "
+ entry.line_number, e);
}
}
}
}
public static void main (String args[]) throws IOException {
Options options = new Options (usage_string, TaskManager.class);
String[] filenames = options.parse_or_usage (args);
if (verbose)
System.out.printf ("Option settings: %s%n", options.settings());
// Make sure at least one file was specified
if (filenames.length == 0) {
options.print_usage ("Error: No task files specified");
System.exit (254);
}
TaskManager tm = new TaskManager(filenames);
// Dump out the tasks
if (verbose) {
System.out.printf ("All tasks:%n");
for (Task task : tm.tasks) {
System.out.printf ("%s\n\n", task.all_vals());
}
}
// Print specified tasks
TaskManager matches = tm.responsible_match (responsible);
matches = matches.milestone_match (milestone);
if (open)
matches = matches.open_only();
if (completed)
matches = matches.completed_only();
switch (format) {
case short_ascii:
System.out.println (matches.toString_short_ascii());
break;
case short_html:
System.out.println (matches.toString_short_html());
break;
case milestone_html:
System.out.println (matches.toString_milestone_html());
break;
}
}
public String toString_short_ascii() {
StringBuilder out = new StringBuilder();
for (Task task : tasks) {
out.append (task.toString_short_ascii() + lineSep);
}
return (out.toString());
}
public String toString_short_html() {
StringBuilder out = new StringBuilder();
double total = 0.0;
String responsible = null;
out.append ("<table>" + lineSep);
for (Task task : tasks) {
if (!task.responsible.equals (responsible)) {
responsible = task.responsible;
total = 0.0;
}
total += (task.duration.floatValue() - task.completed.floatValue());
out.append (task.toString_short_html(total) + lineSep);
}
out.append ("</table>" + lineSep);
return (out.toString());
}
public String toString_milestone_html() {
StringBuilder out = new StringBuilder();
out.append ("<table border cellspacing=0 cellpadding=2>" + lineSep);
out.append ("<tr> <th> Responsible <th> C/D <th> Total <th> Task </tr>"
+ lineSep);
double total = 0.0;
String responsible = null;
for (Task task : tasks) {
if (!task.responsible.equals (responsible)) {
if (responsible != null)
out.append ("<tr bgcolor=grey><td colspan=4></td></tr>" + lineSep);
responsible = task.responsible;
total = 0.0;
}
total += (task.duration.floatValue() - task.completed.floatValue());
out.append (task.toString_milestone_html(total) + lineSep);
}
out.append ("</table>" + lineSep);
return (out.toString());
}
/** Adds the specified task to the end of the task list **/
public void add (Task task) {
tasks.add (task);
}
/**
* Create a new TaskManger with only those tasks assigned to responsible.
* All tasks match a responsible value of null
**/
public TaskManager responsible_match (/*@Nullable*/ String responsible) {
TaskManager tm = new TaskManager();
for (Task task : tasks) {
if ((responsible == null)
|| responsible.equalsIgnoreCase (task.responsible))
tm.add (task);
}
return tm;
}
/** Create a new TaskManger with only those tasks in milestone **/
public TaskManager milestone_match (/*@Nullable*/ String milestone) {
TaskManager tm = new TaskManager();
if (milestone == null)
return tm;
for (Task task : tasks) {
if (milestone.equalsIgnoreCase (task.milestone))
tm.add (task);
}
return tm;
}
/**
* Create a new TaskManger with only completed tasks.
**/
public TaskManager completed_only () {
TaskManager tm = new TaskManager();
for (Task task : tasks) {
if (task.duration <= task.completed)
tm.add (task);
}
return tm;
}
/**
* Create a new TaskManger with only open tasks.
**/
public TaskManager open_only () {
TaskManager tm = new TaskManager();
for (Task task : tasks) {
if (task.duration > task.completed)
tm.add (task);
}
return tm;
}
}