/*
* file: SDEFWriter.java
* author: William (Bill) Iverson
* copyright: (c) GeoComputer 2011
* date: 05/14/2012
*
* started with net.sf.mpxj.mpx MPXWriter.java as template for writing all of below
* so it follows the logic and style of other MPXJ classes
*
* SDEF is the Standard Data Exchange Format, as defined by the USACE (United States
* Army Corp of Engineers). SDEF is a fixed column format text file, used to import
* a project schedule up into the QCS (Quality Control System) software from USACE
*
* Precise specification of SDEF can be found at the USACE library:
* http://140.194.76.129/publications/eng-regs/ER_1-1-11/ER_1-1-11.pdf
*
*/
/*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation; either version 2.1 of the License, or (at your
* option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
*/
package net.sf.mpxj.sdef;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.text.DecimalFormat;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import net.sf.mpxj.Duration;
import net.sf.mpxj.EventManager;
import net.sf.mpxj.ProjectCalendar;
import net.sf.mpxj.ProjectCalendarException;
import net.sf.mpxj.ProjectFile;
import net.sf.mpxj.ProjectProperties;
import net.sf.mpxj.Relation;
import net.sf.mpxj.Task;
import net.sf.mpxj.TimeUnit;
/**
* This class creates a new SDEF file from the contents of
* a ProjectFile instance.
*/
public final class SDEFWriter // extends AbstractProjectWriter
{
private ProjectFile m_projectFile; // from MPXJ library
private EventManager m_eventManager;
private PrintStream m_writer; // line out to a text file
private StringBuilder m_buffer; // used to accumulate characters
private Format m_formatter = new SimpleDateFormat("ddMMMyy"); // USACE required format
private double m_minutesPerDay;
private double m_minutesPerWeek; // needed to get everything into days
private double m_daysPerMonth;
/**
* Write a project file in SDEF format to an output stream.
*
* @param projectFile ProjectFile instance
* @param out output stream
*/
public void write(ProjectFile projectFile, OutputStream out)
{
m_projectFile = projectFile;
m_eventManager = projectFile.getEventManager();
m_writer = new PrintStream(out); // the print stream class is the easiest way to create a text file
m_buffer = new StringBuilder();
try
{
write(); // method call a method, this is how MPXJ is structured, so I followed the lead?
}
catch (Exception e)
{ // used during console debugging
System.out.println("Caught Exception in SDEFWriter.java");
System.out.println(" " + e.toString());
}
finally
{ // keeps things cool after we're done
m_writer = null;
m_projectFile = null;
m_buffer = null;
}
}
/**
* Writes the contents of the project file as MPX records.
*
* @throws IOException
*/
private void write() throws IOException
{
// Following USACE specification from 140.194.76.129/publications/eng-regs/ER_1-1-11/ER_1-1-11.pdf
writeFileCreationRecord(); // VOLM
writeProjectProperties(m_projectFile.getProjectProperties()); // PROJ
writeCalendars(m_projectFile.getCalendars()); // CLDR
writeExceptions(m_projectFile.getCalendars()); // HOLI
writeTasks(m_projectFile.getAllTasks()); // ACTV
writePredecessors(m_projectFile.getAllTasks()); // PRED
// skipped UNIT cost record for now
writeProgress(m_projectFile.getAllTasks()); // PROG
m_writer.println("END"); // last line, that's the end!!!
}
/**
* Write file creation record.
*
* @throws IOException
*/
private void writeFileCreationRecord() throws IOException
{
m_writer.println("VOLM 1"); // first line in file
}
/**
* Write project properties.
*
* @param record project properties
* @throws IOException
*
*/
private void writeProjectProperties(ProjectProperties record) throws IOException
{
// the ProjectProperties class from MPXJ has the details of how many days per week etc....
// so I've assigned these variables in here, but actually use them in other methods
// see the write task method, that's where they're used, but that method only has a Task object
m_minutesPerDay = record.getMinutesPerDay().doubleValue();
m_minutesPerWeek = record.getMinutesPerWeek().doubleValue();
m_daysPerMonth = record.getDaysPerMonth().doubleValue();
// reset buffer to be empty, then concatenate data as required by USACE
m_buffer.setLength(0);
m_buffer.append("PROJ ");
m_buffer.append(m_formatter.format(record.getStartDate()).toUpperCase() + " "); // DataDate
m_buffer.append(SDEFmethods.lset(record.getManager(), 4) + " "); // ProjIdent
m_buffer.append(SDEFmethods.lset(record.getProjectTitle(), 48) + " "); // ProjName
m_buffer.append(SDEFmethods.lset(record.getSubject(), 36) + " "); // ContrName
m_buffer.append("P "); // ArrowP
m_buffer.append(SDEFmethods.lset(record.getKeywords(), 7)); // ContractNum
m_buffer.append(m_formatter.format(record.getStartDate()).toUpperCase() + " "); // ProjStart
m_buffer.append(m_formatter.format(record.getFinishDate()).toUpperCase()); // ProjEnd
m_writer.println(m_buffer);
}
/**
* This will create a line in the SDEF file for each calendar
* if there are more than 9 calendars, you'll have a big error,
* as USACE numbers them 0-9.
*
* @param records list of ProjectCalendar instances
*/
private void writeCalendars(List<ProjectCalendar> records)
{
//
// Write project calendars
//
for (ProjectCalendar record : records)
{
m_buffer.setLength(0);
m_buffer.append("CLDR ");
m_buffer.append(SDEFmethods.lset(record.getUniqueID().toString(), 2)); // 2 character used, USACE allows 1
String workDays = SDEFmethods.workDays(record); // custom line, like NYYYYYN for a week
m_buffer.append(SDEFmethods.lset(workDays, 8));
m_buffer.append(SDEFmethods.lset(record.getName(), 30));
m_writer.println(m_buffer);
}
}
/**
* Write calendar exceptions.
*
* @param records list of ProjectCalendars
* @throws IOException
*/
private void writeExceptions(List<ProjectCalendar> records) throws IOException
{
for (ProjectCalendar record : records)
{
if (!record.getCalendarExceptions().isEmpty())
{
// Need to move HOLI up here and get 15 exceptions per line as per USACE spec.
// for now, we'll write one line for each calendar exception, hope there aren't too many
//
// changing this would be a serious upgrade, too much coding to do today....
for (ProjectCalendarException ex : record.getCalendarExceptions())
{
writeCalendarException(record, ex);
}
}
m_eventManager.fireCalendarWrittenEvent(record); // left here from MPX template, maybe not needed???
}
}
/**
* Write a calendar exception.
*
* @param parentCalendar parent calendar instance
* @param record calendar exception instance
* @throws IOException
*/
private void writeCalendarException(ProjectCalendar parentCalendar, ProjectCalendarException record) throws IOException
{
m_buffer.setLength(0);
Calendar stepDay = Calendar.getInstance();
stepDay.setTime(record.getFromDate()); // Start at From Date, then step through days...
Calendar lastDay = Calendar.getInstance();
lastDay.setTime(record.getToDate()); // last day in this exception
m_buffer.append("HOLI ");
m_buffer.append(SDEFmethods.lset(parentCalendar.getUniqueID().toString(), 2));
while (stepDay.compareTo(lastDay) <= 0)
{
m_buffer.append(m_formatter.format(stepDay.getTime()).toUpperCase() + " ");
stepDay.add(Calendar.DAY_OF_MONTH, 1);
}
m_writer.println(m_buffer.toString());
}
/**
* Write a task.
*
* @param record task instance
* @throws IOException
*/
private void writeTask(Task record) throws IOException
{
m_buffer.setLength(0);
if (!record.getSummary())
{
m_buffer.append("ACTV ");
m_buffer.append(SDEFmethods.rset(record.getUniqueID().toString(), 10) + " ");
m_buffer.append(SDEFmethods.lset(record.getName(), 30) + " ");
// Following just makes certain we have days for duration, as per USACE spec.
Duration dd = record.getDuration();
double duration = dd.getDuration();
if (dd.getUnits() != TimeUnit.DAYS)
{
dd = Duration.convertUnits(duration, dd.getUnits(), TimeUnit.DAYS, m_minutesPerDay, m_minutesPerWeek, m_daysPerMonth);
}
Double days = Double.valueOf(dd.getDuration() + 0.5); // Add 0.5 so half day rounds up upon truncation
Integer est = Integer.valueOf(days.intValue());
m_buffer.append(SDEFmethods.rset(est.toString(), 3) + " "); // task duration in days required by USACE
String conType = "ES "; // assume early start
Date conDate = record.getEarlyStart();
int test = record.getConstraintType().getValue(); // test for other types
if (test == 1 || test == 3 || test == 6 || test == 7)
{
conType = "LF "; // see ConstraintType enum for definitions
conDate = record.getLateFinish();
}
m_buffer.append(m_formatter.format(conDate).toUpperCase() + " "); // Constraint Date
m_buffer.append(conType); // Constraint Type
if (record.getCalendar() == null)
{
m_buffer.append("1 ");
}
else
{
m_buffer.append(SDEFmethods.lset(record.getCalendar().getUniqueID().toString(), 1) + " ");
}
// skipping hammock code in here
// use of text fields for extra USACE data is suggested at my web site: www.geocomputer.com
// not documented on how to do this here, so I need to comment out at present
// m_buffer.append(SDEFmethods.Lset(record.getText1(), 3) + " ");
// m_buffer.append(SDEFmethods.Lset(record.getText2(), 4) + " ");
// m_buffer.append(SDEFmethods.Lset(record.getText3(), 4) + " ");
// m_buffer.append(SDEFmethods.Lset(record.getText4(), 6) + " ");
// m_buffer.append(SDEFmethods.Lset(record.getText5(), 6) + " ");
// m_buffer.append(SDEFmethods.Lset(record.getText6(), 2) + " ");
// m_buffer.append(SDEFmethods.Lset(record.getText7(), 1) + " ");
// m_buffer.append(SDEFmethods.Lset(record.getText8(), 30) + " ");
m_writer.println(m_buffer.toString());
m_eventManager.fireTaskWrittenEvent(record);
}
}
/**
* Write an SDEF line for each task ACTV.
*
* @param tasks list of Task instances
* @throws IOException
*/
private void writeTasks(List<Task> tasks) throws IOException
{
for (Task task : tasks)
{
writeTask(task); // writes one line to SDEF file
}
}
/**
* For each task, write an SDEF line for each PRED.
*
* @param tasks list of Task instances
*/
private void writePredecessors(List<Task> tasks)
{
for (Task task : tasks)
{
writeTaskPredecessors(task);
}
}
/**
* Write each predecessor for a task.
*
* @param record Task instance
*/
private void writeTaskPredecessors(Task record)
{
m_buffer.setLength(0);
//
// Write the task predecessor
//
if (!record.getSummary() && record.getPredecessors() != null)
{ // I don't use summary tasks for SDEF
m_buffer.append("PRED ");
List<Relation> predecessors = record.getPredecessors();
for (Relation pred : predecessors)
{
m_buffer.append(SDEFmethods.rset(pred.getSourceTask().getUniqueID().toString(), 10) + " ");
m_buffer.append(SDEFmethods.rset(pred.getTargetTask().getUniqueID().toString(), 10) + " ");
String type = "C"; // default finish-to-start
if (!pred.getType().toString().equals("FS"))
{
type = pred.getType().toString().substring(0, 1);
}
m_buffer.append(type + " ");
Duration dd = pred.getLag();
double duration = dd.getDuration();
if (dd.getUnits() != TimeUnit.DAYS)
{
dd = Duration.convertUnits(duration, dd.getUnits(), TimeUnit.DAYS, m_minutesPerDay, m_minutesPerWeek, m_daysPerMonth);
}
Double days = Double.valueOf(dd.getDuration() + 0.5); // Add 0.5 so half day rounds up upon truncation
Integer est = Integer.valueOf(days.intValue());
m_buffer.append(SDEFmethods.rset(est.toString(), 4) + " "); // task duration in days required by USACE
}
m_writer.println(m_buffer.toString());
}
}
/**
* Writes a progress line to the SDEF file.
*
* Progress lines in SDEF are a little tricky, you need to assume a percent complete
* this could be physical or temporal, I don't know what you're using???
* So in this version of SDEFwriter, I just put in 0.00 for cost progress to date, see *** below
*
* @param record Task instance
*/
private void writePROG(Task record)
{
m_buffer.setLength(0);
//
// Write the progress record
//
if (!record.getSummary())
{ // I don't use summary tasks for SDEF
m_buffer.append("PROG ");
m_buffer.append(SDEFmethods.rset(record.getUniqueID().toString(), 10) + " ");
Date temp = record.getActualStart();
if (temp == null)
{
m_buffer.append(" "); // SDEf is column sensitive, so the number of blanks here is crucial
}
else
{
m_buffer.append(m_formatter.format(record.getActualStart()).toUpperCase() + " "); // ACTUAL START DATE
}
temp = record.getActualFinish();
if (temp == null)
{
m_buffer.append(" ");
}
else
{
m_buffer.append(m_formatter.format(record.getActualFinish()).toUpperCase() + " "); // ACTUAL FINISH DATE
}
Duration dd = record.getRemainingDuration();
double duration = dd.getDuration();
if (dd.getUnits() != TimeUnit.DAYS)
{
dd = Duration.convertUnits(duration, dd.getUnits(), TimeUnit.DAYS, m_minutesPerDay, m_minutesPerWeek, m_daysPerMonth);
}
Double days = Double.valueOf(dd.getDuration() + 0.5); // Add 0.5 so half day rounds up upon truncation
Integer est = Integer.valueOf(days.intValue());
m_buffer.append(SDEFmethods.rset(est.toString(), 3) + " "); // task duration in days required by USACE
DecimalFormat twoDec = new DecimalFormat("#0.00"); // USACE required currency format
m_buffer.append(SDEFmethods.rset(twoDec.format(record.getCost().floatValue()), 12) + " ");
m_buffer.append(SDEFmethods.rset(twoDec.format(0.00), 12) + " "); // *** assume zero progress on cost
m_buffer.append(SDEFmethods.rset(twoDec.format(0.00), 12) + " "); // *** assume zero progress on cost
m_buffer.append(m_formatter.format(record.getEarlyStart()).toUpperCase() + " ");
m_buffer.append(m_formatter.format(record.getEarlyFinish()).toUpperCase() + " ");
m_buffer.append(m_formatter.format(record.getLateStart()).toUpperCase() + " ");
m_buffer.append(m_formatter.format(record.getLateFinish()).toUpperCase() + " ");
dd = record.getTotalSlack();
duration = dd.getDuration();
if (dd.getUnits() != TimeUnit.DAYS)
{
dd = Duration.convertUnits(duration, dd.getUnits(), TimeUnit.DAYS, m_minutesPerDay, m_minutesPerWeek, m_daysPerMonth);
}
days = Double.valueOf(dd.getDuration() + 0.5); // Add 0.5 so half day rounds up upon truncation
est = Integer.valueOf(days.intValue());
char slack;
if (est.intValue() >= 0)
{
slack = '+'; // USACE likes positive slack, so they separate the sign from the value
}
else
{
slack = '-'; // only write a negative when it's negative, i.e. can't be done in project management terms!!!
}
m_buffer.append(slack + " ");
est = Integer.valueOf(Math.abs(days.intValue()));
m_buffer.append(SDEFmethods.rset(est.toString(), 4)); // task duration in days required by USACE
m_writer.println(m_buffer.toString());
m_eventManager.fireTaskWrittenEvent(record);
}
}
/**
* Write a progress line for each task.
*
* @param tasks list of Task instances
*/
private void writeProgress(List<Task> tasks)
{
for (Task task : tasks)
{
writePROG(task);
}
}
}