/*
* file: MPXReader.java
* author: Jon Iles
* copyright: (c) Packwood Software 2005
* date: Jan 3, 2006
*/
/*
* 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.mpx;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import net.sf.mpxj.DateRange;
import net.sf.mpxj.Day;
import net.sf.mpxj.DayType;
import net.sf.mpxj.Duration;
import net.sf.mpxj.EventManager;
import net.sf.mpxj.FileVersion;
import net.sf.mpxj.MPXJException;
import net.sf.mpxj.ProjectCalendar;
import net.sf.mpxj.ProjectCalendarException;
import net.sf.mpxj.ProjectCalendarHours;
import net.sf.mpxj.ProjectConfig;
import net.sf.mpxj.ProjectFile;
import net.sf.mpxj.ProjectProperties;
import net.sf.mpxj.RecurringTask;
import net.sf.mpxj.Relation;
import net.sf.mpxj.RelationType;
import net.sf.mpxj.Resource;
import net.sf.mpxj.ResourceAssignment;
import net.sf.mpxj.ResourceAssignmentWorkgroupFields;
import net.sf.mpxj.ResourceField;
import net.sf.mpxj.Task;
import net.sf.mpxj.TaskField;
import net.sf.mpxj.TaskType;
import net.sf.mpxj.TimeUnit;
import net.sf.mpxj.common.InputStreamTokenizer;
import net.sf.mpxj.common.NumberHelper;
import net.sf.mpxj.common.ReaderTokenizer;
import net.sf.mpxj.common.Tokenizer;
import net.sf.mpxj.listener.ProjectListener;
import net.sf.mpxj.reader.AbstractProjectReader;
/**
* This class creates a new ProjectFile instance by reading an MPX file.
*/
public final class MPXReader extends AbstractProjectReader
{
/**
* {@inheritDoc}
*/
@Override public void addProjectListener(ProjectListener listener)
{
if (m_projectListeners == null)
{
m_projectListeners = new LinkedList<ProjectListener>();
}
m_projectListeners.add(listener);
}
/**
* {@inheritDoc}
*/
@Override public ProjectFile read(InputStream is) throws MPXJException
{
int line = 1;
try
{
//
// Test the header and extract the separator. If this is successful,
// we reset the stream back as far as we can. The design of the
// BufferedInputStream class means that we can't get back to character
// zero, so the first record we will read will get "PX" rather than
// "MPX" in the first field position.
//
BufferedInputStream bis = new BufferedInputStream(is);
byte[] data = new byte[4];
data[0] = (byte) bis.read();
bis.mark(1024);
data[1] = (byte) bis.read();
data[2] = (byte) bis.read();
data[3] = (byte) bis.read();
if ((data[0] != 'M') || (data[1] != 'P') || (data[2] != 'X'))
{
throw new MPXJException(MPXJException.INVALID_FILE);
}
m_projectFile = new ProjectFile();
m_eventManager = m_projectFile.getEventManager();
m_projectConfig = m_projectFile.getProjectConfig();
m_projectConfig.setAutoTaskID(false);
m_projectConfig.setAutoTaskUniqueID(false);
m_projectConfig.setAutoResourceID(false);
m_projectConfig.setAutoResourceUniqueID(false);
m_projectConfig.setAutoOutlineLevel(false);
m_projectConfig.setAutoOutlineNumber(false);
m_projectConfig.setAutoWBS(false);
m_eventManager.addProjectListeners(m_projectListeners);
LocaleUtility.setLocale(m_projectFile.getProjectProperties(), m_locale);
m_delimiter = (char) data[3];
m_projectFile.getProjectProperties().setMpxDelimiter(m_delimiter);
m_taskModel = new TaskModel(m_projectFile, m_locale);
m_taskModel.setLocale(m_locale);
m_resourceModel = new ResourceModel(m_projectFile, m_locale);
m_resourceModel.setLocale(m_locale);
m_baseOutlineLevel = -1;
m_formats = new MPXJFormats(m_locale, LocaleData.getString(m_locale, LocaleData.NA), m_projectFile);
m_deferredRelationships = new LinkedList<DeferredRelationship>();
bis.reset();
//
// Read the file creation record. At this point we are reading
// directly from an input stream so no character set decoding is
// taking place. We assume that any text in this record will not
// require decoding.
//
Tokenizer tk = new InputStreamTokenizer(bis);
tk.setDelimiter(m_delimiter);
Record record;
String number;
//
// Add the header record
//
parseRecord(Integer.toString(MPXConstants.FILE_CREATION_RECORD_NUMBER), new Record(m_locale, tk, m_formats));
++line;
//
// Now process the remainder of the file in full. As we have read the
// file creation record we have access to the field which specifies the
// codepage used to encode the character set in this file. We set up
// an input stream reader using the appropriate character set, and
// create a new tokenizer to read from this Reader instance.
//
InputStreamReader reader = new InputStreamReader(bis, m_projectFile.getProjectProperties().getMpxCodePage().getCharset());
tk = new ReaderTokenizer(reader);
tk.setDelimiter(m_delimiter);
//
// Read the remainder of the records
//
while (tk.getType() != Tokenizer.TT_EOF)
{
record = new Record(m_locale, tk, m_formats);
number = record.getRecordNumber();
if (number != null)
{
parseRecord(number, record);
}
++line;
}
processDeferredRelationships();
//
// Ensure that the structure is consistent
//
m_projectFile.updateStructure();
//
// Ensure that the unique ID counters are correct
//
m_projectConfig.updateUniqueCounters();
m_projectConfig.setAutoCalendarUniqueID(false);
return (m_projectFile);
}
catch (Exception ex)
{
throw new MPXJException(MPXJException.READ_ERROR + " (failed at line " + line + ")", ex);
}
finally
{
m_projectFile = null;
m_lastTask = null;
m_lastResource = null;
m_lastResourceCalendar = null;
m_lastResourceAssignment = null;
m_lastBaseCalendar = null;
m_resourceTableDefinition = false;
m_taskTableDefinition = false;
m_taskModel = null;
m_resourceModel = null;
m_formats = null;
m_deferredRelationships = null;
}
}
/**
* Parse an MPX record.
*
* @param recordNumber record number
* @param record record data
* @throws MPXJException
*/
private void parseRecord(String recordNumber, Record record) throws MPXJException
{
switch (Integer.parseInt(recordNumber))
{
case MPXConstants.PROJECT_NAMES_RECORD_NUMBER:
case MPXConstants.DDE_OLE_CLIENT_LINKS_RECORD_NUMBER:
case MPXConstants.COMMENTS_RECORD_NUMBER:
{
// silently ignored
break;
}
case MPXConstants.CURRENCY_SETTINGS_RECORD_NUMBER:
{
populateCurrencySettings(record, m_projectFile.getProjectProperties());
m_formats.update();
break;
}
case MPXConstants.DEFAULT_SETTINGS_RECORD_NUMBER:
{
populateDefaultSettings(record, m_projectFile.getProjectProperties());
m_formats.update();
break;
}
case MPXConstants.DATE_TIME_SETTINGS_RECORD_NUMBER:
{
populateDateTimeSettings(record, m_projectFile.getProjectProperties());
m_formats.update();
break;
}
case MPXConstants.BASE_CALENDAR_RECORD_NUMBER:
{
m_lastBaseCalendar = m_projectFile.addCalendar();
populateCalendar(record, m_lastBaseCalendar, true);
break;
}
case MPXConstants.BASE_CALENDAR_HOURS_RECORD_NUMBER:
{
if (m_lastBaseCalendar != null)
{
ProjectCalendarHours hours = m_lastBaseCalendar.addCalendarHours();
populateCalendarHours(record, hours);
}
break;
}
case MPXConstants.BASE_CALENDAR_EXCEPTION_RECORD_NUMBER:
{
if (m_lastBaseCalendar != null)
{
populateCalendarException(record, m_lastBaseCalendar);
}
break;
}
case MPXConstants.PROJECT_HEADER_RECORD_NUMBER:
{
populateProjectHeader(record, m_projectFile.getProjectProperties());
m_formats.update();
break;
}
case MPXConstants.RESOURCE_MODEL_TEXT_RECORD_NUMBER:
{
if ((m_resourceTableDefinition == false) && (m_ignoreTextModels == false))
{
m_resourceModel.update(record, true);
m_resourceTableDefinition = true;
}
break;
}
case MPXConstants.RESOURCE_MODEL_NUMERIC_RECORD_NUMBER:
{
if (m_resourceTableDefinition == false)
{
m_resourceModel.update(record, false);
m_resourceTableDefinition = true;
}
break;
}
case MPXConstants.RESOURCE_RECORD_NUMBER:
{
m_lastResource = m_projectFile.addResource();
populateResource(m_lastResource, record);
m_eventManager.fireResourceReadEvent(m_lastResource);
break;
}
case MPXConstants.RESOURCE_NOTES_RECORD_NUMBER:
{
if (m_lastResource != null)
{
m_lastResource.setNotes(record.getString(0));
}
break;
}
case MPXConstants.RESOURCE_CALENDAR_RECORD_NUMBER:
{
if (m_lastResource != null)
{
m_lastResourceCalendar = m_lastResource.addResourceCalendar();
populateCalendar(record, m_lastResourceCalendar, false);
}
break;
}
case MPXConstants.RESOURCE_CALENDAR_HOURS_RECORD_NUMBER:
{
if (m_lastResourceCalendar != null)
{
ProjectCalendarHours hours = m_lastResourceCalendar.addCalendarHours();
populateCalendarHours(record, hours);
}
break;
}
case MPXConstants.RESOURCE_CALENDAR_EXCEPTION_RECORD_NUMBER:
{
if (m_lastResourceCalendar != null)
{
populateCalendarException(record, m_lastResourceCalendar);
}
break;
}
case MPXConstants.TASK_MODEL_TEXT_RECORD_NUMBER:
{
if ((m_taskTableDefinition == false) && (m_ignoreTextModels == false))
{
m_taskModel.update(record, true);
m_taskTableDefinition = true;
}
break;
}
case MPXConstants.TASK_MODEL_NUMERIC_RECORD_NUMBER:
{
if (m_taskTableDefinition == false)
{
m_taskModel.update(record, false);
m_taskTableDefinition = true;
}
break;
}
case MPXConstants.TASK_RECORD_NUMBER:
{
m_lastTask = m_projectFile.addTask();
populateTask(record, m_lastTask);
int outlineLevel = NumberHelper.getInt(m_lastTask.getOutlineLevel());
if (m_baseOutlineLevel == -1)
{
m_baseOutlineLevel = outlineLevel;
}
if (outlineLevel != m_baseOutlineLevel)
{
List<Task> childTasks = m_projectFile.getChildTasks();
if (childTasks.isEmpty() == true)
{
throw new MPXJException(MPXJException.INVALID_OUTLINE);
}
childTasks.get(childTasks.size() - 1).addChildTask(m_lastTask, outlineLevel);
}
m_eventManager.fireTaskReadEvent(m_lastTask);
break;
}
case MPXConstants.TASK_NOTES_RECORD_NUMBER:
{
if (m_lastTask != null)
{
m_lastTask.setNotes(record.getString(0));
}
break;
}
case MPXConstants.RECURRING_TASK_RECORD_NUMBER:
{
if (m_lastTask != null)
{
m_lastTask.setRecurring(true);
RecurringTask task = m_lastTask.addRecurringTask();
populateRecurringTask(record, task);
}
break;
}
case MPXConstants.RESOURCE_ASSIGNMENT_RECORD_NUMBER:
{
if (m_lastTask != null)
{
m_lastResourceAssignment = m_lastTask.addResourceAssignment((Resource) null);
populateResourceAssignment(record, m_lastResourceAssignment);
}
break;
}
case MPXConstants.RESOURCE_ASSIGNMENT_WORKGROUP_FIELDS_RECORD_NUMBER:
{
if (m_lastResourceAssignment != null)
{
ResourceAssignmentWorkgroupFields workgroup = m_lastResourceAssignment.addWorkgroupAssignment();
populateResourceAssignmentWorkgroupFields(record, workgroup);
}
break;
}
case MPXConstants.FILE_CREATION_RECORD_NUMBER:
{
populateFileCreationRecord(record, m_projectFile.getProjectProperties());
break;
}
default:
{
throw new MPXJException(MPXJException.INVALID_RECORD);
}
}
}
/**
* Populates currency settings.
*
* @param record MPX record
* @param properties project properties
*/
private void populateCurrencySettings(Record record, ProjectProperties properties)
{
properties.setCurrencySymbol(record.getString(0));
properties.setSymbolPosition(record.getCurrencySymbolPosition(1));
properties.setCurrencyDigits(record.getInteger(2));
Character c = record.getCharacter(3);
if (c != null)
{
properties.setThousandsSeparator(c.charValue());
}
c = record.getCharacter(4);
if (c != null)
{
properties.setDecimalSeparator(c.charValue());
}
}
/**
* Populates default settings.
*
* @param record MPX record
* @param properties project properties
* @throws MPXJException
*/
private void populateDefaultSettings(Record record, ProjectProperties properties) throws MPXJException
{
properties.setDefaultDurationUnits(record.getTimeUnit(0));
properties.setDefaultDurationIsFixed(record.getNumericBoolean(1));
properties.setDefaultWorkUnits(record.getTimeUnit(2));
properties.setMinutesPerDay(Double.valueOf(NumberHelper.getDouble(record.getFloat(3)) * 60));
properties.setMinutesPerWeek(Double.valueOf(NumberHelper.getDouble(record.getFloat(4)) * 60));
properties.setDefaultStandardRate(record.getRate(5));
properties.setDefaultOvertimeRate(record.getRate(6));
properties.setUpdatingTaskStatusUpdatesResourceStatus(record.getNumericBoolean(7));
properties.setSplitInProgressTasks(record.getNumericBoolean(8));
}
/**
* Populates date time settings.
*
* @param record MPX record
* @param properties project properties
*/
private void populateDateTimeSettings(Record record, ProjectProperties properties)
{
properties.setDateOrder(record.getDateOrder(0));
properties.setTimeFormat(record.getTimeFormat(1));
Date time = getTimeFromInteger(record.getInteger(2));
if (time != null)
{
properties.setDefaultStartTime(time);
}
Character c = record.getCharacter(3);
if (c != null)
{
properties.setDateSeparator(c.charValue());
}
c = record.getCharacter(4);
if (c != null)
{
properties.setTimeSeparator(c.charValue());
}
properties.setAMText(record.getString(5));
properties.setPMText(record.getString(6));
properties.setDateFormat(record.getDateFormat(7));
properties.setBarTextDateFormat(record.getDateFormat(8));
}
/**
* Converts a time represented as an integer to a Date instance.
*
* @param time integer time
* @return Date instance
*/
private Date getTimeFromInteger(Integer time)
{
Date result = null;
if (time != null)
{
int minutes = time.intValue();
int hours = minutes / 60;
minutes -= (hours * 60);
Calendar cal = Calendar.getInstance();
cal.set(Calendar.MILLISECOND, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MINUTE, minutes);
cal.set(Calendar.HOUR_OF_DAY, hours);
result = cal.getTime();
}
return (result);
}
/**
* Populates the project header.
*
* @param record MPX record
* @param properties project properties
* @throws MPXJException
*/
private void populateProjectHeader(Record record, ProjectProperties properties) throws MPXJException
{
properties.setProjectTitle(record.getString(0));
properties.setCompany(record.getString(1));
properties.setManager(record.getString(2));
properties.setDefaultCalendarName(record.getString(3));
properties.setStartDate(record.getDateTime(4));
properties.setFinishDate(record.getDateTime(5));
properties.setScheduleFrom(record.getScheduleFrom(6));
properties.setCurrentDate(record.getDateTime(7));
properties.setComments(record.getString(8));
properties.setCost(record.getCurrency(9));
properties.setBaselineCost(record.getCurrency(10));
properties.setActualCost(record.getCurrency(11));
properties.setWork(record.getDuration(12));
properties.setBaselineWork(record.getDuration(13));
properties.setActualWork(record.getDuration(14));
properties.setWork2(record.getPercentage(15));
properties.setDuration(record.getDuration(16));
properties.setBaselineDuration(record.getDuration(17));
properties.setActualDuration(record.getDuration(18));
properties.setPercentageComplete(record.getPercentage(19));
properties.setBaselineStart(record.getDateTime(20));
properties.setBaselineFinish(record.getDateTime(21));
properties.setActualStart(record.getDateTime(22));
properties.setActualFinish(record.getDateTime(23));
properties.setStartVariance(record.getDuration(24));
properties.setFinishVariance(record.getDuration(25));
properties.setSubject(record.getString(26));
properties.setAuthor(record.getString(27));
properties.setKeywords(record.getString(28));
}
/**
* Populates a calendar hours instance.
*
* @param record MPX record
* @param hours calendar hours instance
* @throws MPXJException
*/
private void populateCalendarHours(Record record, ProjectCalendarHours hours) throws MPXJException
{
hours.setDay(Day.getInstance(NumberHelper.getInt(record.getInteger(0))));
addDateRange(hours, record.getTime(1), record.getTime(2));
addDateRange(hours, record.getTime(3), record.getTime(4));
addDateRange(hours, record.getTime(5), record.getTime(6));
}
/**
* Get a date range that correctly handles the cae where the end time
* is midnight. In this instance the end time should be the start of the
* next day.
*
* @param hours calendar hours
* @param start start date
* @param end end date
*/
private void addDateRange(ProjectCalendarHours hours, Date start, Date end)
{
if (start != null && end != null)
{
Calendar cal = Calendar.getInstance();
cal.setTime(end);
// If the time ends on midnight, the date should be the next day. Otherwise problems occur.
if (cal.get(Calendar.HOUR_OF_DAY) == 0 && cal.get(Calendar.MINUTE) == 0 && cal.get(Calendar.SECOND) == 0 && cal.get(Calendar.MILLISECOND) == 0)
{
cal.add(Calendar.DAY_OF_YEAR, 1);
}
end = cal.getTime();
hours.addRange(new DateRange(start, end));
}
}
/**
* Populates a calendar exception instance.
*
* @param record MPX record
* @param calendar calendar to which the exception will be added
* @throws MPXJException
*/
private void populateCalendarException(Record record, ProjectCalendar calendar) throws MPXJException
{
Date fromDate = record.getDate(0);
Date toDate = record.getDate(1);
boolean working = record.getNumericBoolean(2);
ProjectCalendarException exception = calendar.addCalendarException(fromDate, toDate);
if (working)
{
exception.addRange(new DateRange(record.getTime(3), record.getTime(4)));
exception.addRange(new DateRange(record.getTime(5), record.getTime(6)));
exception.addRange(new DateRange(record.getTime(7), record.getTime(8)));
}
}
/**
* Populates a calendar instance.
*
* @param record MPX record
* @param calendar calendar instance
* @param isBaseCalendar true if this is a base calendar
*/
private void populateCalendar(Record record, ProjectCalendar calendar, boolean isBaseCalendar)
{
if (isBaseCalendar == true)
{
calendar.setName(record.getString(0));
}
else
{
calendar.setParent(m_projectFile.getCalendarByName(record.getString(0)));
}
calendar.setWorkingDay(Day.SUNDAY, DayType.getInstance(record.getInteger(1)));
calendar.setWorkingDay(Day.MONDAY, DayType.getInstance(record.getInteger(2)));
calendar.setWorkingDay(Day.TUESDAY, DayType.getInstance(record.getInteger(3)));
calendar.setWorkingDay(Day.WEDNESDAY, DayType.getInstance(record.getInteger(4)));
calendar.setWorkingDay(Day.THURSDAY, DayType.getInstance(record.getInteger(5)));
calendar.setWorkingDay(Day.FRIDAY, DayType.getInstance(record.getInteger(6)));
calendar.setWorkingDay(Day.SATURDAY, DayType.getInstance(record.getInteger(7)));
m_eventManager.fireCalendarReadEvent(calendar);
}
/**
* Populates a resource.
*
* @param resource resource instance
* @param record MPX record
* @throws MPXJException
*/
private void populateResource(Resource resource, Record record) throws MPXJException
{
String falseText = LocaleData.getString(m_locale, LocaleData.NO);
int length = record.getLength();
int[] model = m_resourceModel.getModel();
for (int i = 0; i < length; i++)
{
int mpxFieldType = model[i];
if (mpxFieldType == -1)
{
break;
}
String field = record.getString(i);
if (field == null || field.length() == 0)
{
continue;
}
ResourceField resourceField = MPXResourceField.getMpxjField(mpxFieldType);
switch (resourceField)
{
case OBJECTS:
{
resource.set(resourceField, record.getInteger(i));
break;
}
case ID:
{
resource.setID(record.getInteger(i));
break;
}
case UNIQUE_ID:
{
resource.setUniqueID(record.getInteger(i));
break;
}
case MAX_UNITS:
{
resource.set(resourceField, record.getUnits(i));
break;
}
case PERCENT_WORK_COMPLETE:
case PEAK:
{
resource.set(resourceField, record.getPercentage(i));
break;
}
case COST:
case COST_PER_USE:
case COST_VARIANCE:
case BASELINE_COST:
case ACTUAL_COST:
case REMAINING_COST:
{
resource.set(resourceField, record.getCurrency(i));
break;
}
case OVERTIME_RATE:
case STANDARD_RATE:
{
resource.set(resourceField, record.getRate(i));
break;
}
case REMAINING_WORK:
case OVERTIME_WORK:
case BASELINE_WORK:
case ACTUAL_WORK:
case WORK:
case WORK_VARIANCE:
{
resource.set(resourceField, record.getDuration(i));
break;
}
case ACCRUE_AT:
{
resource.set(resourceField, record.getAccrueType(i));
break;
}
case LINKED_FIELDS:
case OVERALLOCATED:
{
resource.set(resourceField, record.getBoolean(i, falseText));
break;
}
default:
{
resource.set(resourceField, field);
break;
}
}
}
if (m_projectConfig.getAutoResourceUniqueID() == true)
{
resource.setUniqueID(Integer.valueOf(m_projectConfig.getNextResourceUniqueID()));
}
if (m_projectConfig.getAutoResourceID() == true)
{
resource.setID(Integer.valueOf(m_projectConfig.getNextResourceID()));
}
//
// Handle malformed MPX files - ensure we have a unique ID
//
if (resource.getUniqueID() == null)
{
resource.setUniqueID(resource.getID());
}
}
/**
* Populates a relation list.
*
* @param task parent task
* @param field target task field
* @param data MPX relation list data
*/
private void populateRelationList(Task task, TaskField field, String data)
{
DeferredRelationship dr = new DeferredRelationship();
dr.setTask(task);
dr.setField(field);
dr.setData(data);
m_deferredRelationships.add(dr);
}
/**
* This method iterates through the deferred relationships,
* parsing the data and setting up relationships between tasks.
*
* @throws MPXJException
*/
private void processDeferredRelationships() throws MPXJException
{
for (DeferredRelationship dr : m_deferredRelationships)
{
processDeferredRelationship(dr);
}
}
/**
* This method processes a single deferred relationship list.
*
* @param dr deferred relationship list data
* @throws MPXJException
*/
private void processDeferredRelationship(DeferredRelationship dr) throws MPXJException
{
String data = dr.getData();
Task task = dr.getTask();
int length = data.length();
if (length != 0)
{
int start = 0;
int end = 0;
while (end != length)
{
end = data.indexOf(m_delimiter, start);
if (end == -1)
{
end = length;
}
populateRelation(dr.getField(), task, data.substring(start, end).trim());
start = end + 1;
}
}
}
/**
* Creates and populates a new task relationship.
*
* @param field which task field source of data
* @param sourceTask relationship source task
* @param relationship relationship string
* @throws MPXJException
*/
private void populateRelation(TaskField field, Task sourceTask, String relationship) throws MPXJException
{
int index = 0;
int length = relationship.length();
//
// Extract the identifier
//
while ((index < length) && (Character.isDigit(relationship.charAt(index)) == true))
{
++index;
}
Integer taskID;
try
{
taskID = Integer.valueOf(relationship.substring(0, index));
}
catch (NumberFormatException ex)
{
throw new MPXJException(MPXJException.INVALID_FORMAT + " '" + relationship + "'");
}
//
// Now find the task, so we can extract the unique ID
//
Task targetTask;
if (field == TaskField.PREDECESSORS)
{
targetTask = m_projectFile.getTaskByID(taskID);
}
else
{
targetTask = m_projectFile.getTaskByUniqueID(taskID);
}
//
// If we haven't reached the end, we next expect to find
// SF, SS, FS, FF
//
RelationType type = null;
Duration lag = null;
if (index == length)
{
type = RelationType.FINISH_START;
lag = Duration.getInstance(0, TimeUnit.DAYS);
}
else
{
if ((index + 1) == length)
{
throw new MPXJException(MPXJException.INVALID_FORMAT + " '" + relationship + "'");
}
type = RelationTypeUtility.getInstance(m_locale, relationship.substring(index, index + 2));
index += 2;
if (index == length)
{
lag = Duration.getInstance(0, TimeUnit.DAYS);
}
else
{
if (relationship.charAt(index) == '+')
{
++index;
}
lag = DurationUtility.getInstance(relationship.substring(index), m_formats.getDurationDecimalFormat(), m_locale);
}
}
if (type == null)
{
throw new MPXJException(MPXJException.INVALID_FORMAT + " '" + relationship + "'");
}
Relation relation = sourceTask.addPredecessor(targetTask, type, lag);
m_eventManager.fireRelationReadEvent(relation);
}
/**
* Populates a task instance.
*
* @param record MPX record
* @param task task instance
* @throws MPXJException
*/
private void populateTask(Record record, Task task) throws MPXJException
{
String falseText = LocaleData.getString(m_locale, LocaleData.NO);
int mpxFieldID = 0;
String field;
int i = 0;
int length = record.getLength();
int[] model = m_taskModel.getModel();
while (i < length)
{
mpxFieldID = model[i];
if (mpxFieldID == -1)
{
break;
}
field = record.getString(i++);
if ((field == null) || (field.length() == 0))
{
continue;
}
TaskField taskField = MPXTaskField.getMpxjField(mpxFieldID);
if (taskField == null)
{
System.out.println("Null Task Field " + mpxFieldID);
continue;
}
switch (taskField)
{
case PREDECESSORS:
case UNIQUE_ID_PREDECESSORS:
{
populateRelationList(task, taskField, field);
break;
}
case PERCENT_COMPLETE:
case PERCENT_WORK_COMPLETE:
{
try
{
task.set(taskField, m_formats.getPercentageDecimalFormat().parse(field));
}
catch (ParseException ex)
{
throw new MPXJException("Failed to parse percentage", ex);
}
break;
}
case ACTUAL_COST:
case BASELINE_COST:
case BCWP:
case BCWS:
case COST:
case COST1:
case COST2:
case COST3:
case COST_VARIANCE:
case CV:
case FIXED_COST:
case REMAINING_COST:
case SV:
{
try
{
task.set(taskField, m_formats.getCurrencyFormat().parse(field));
}
catch (ParseException ex)
{
throw new MPXJException("Failed to parse currency", ex);
}
break;
}
case ACTUAL_DURATION:
case ACTUAL_WORK:
case BASELINE_DURATION:
case BASELINE_WORK:
case DURATION:
case DURATION1:
case DURATION2:
case DURATION3:
case DURATION_VARIANCE:
case FINISH_VARIANCE:
case FREE_SLACK:
case REMAINING_DURATION:
case REMAINING_WORK:
case START_VARIANCE:
case TOTAL_SLACK:
case WORK:
case WORK_VARIANCE:
case LEVELING_DELAY:
{
task.set(taskField, DurationUtility.getInstance(field, m_formats.getDurationDecimalFormat(), m_locale));
break;
}
case ACTUAL_FINISH:
case ACTUAL_START:
case BASELINE_FINISH:
case BASELINE_START:
case CONSTRAINT_DATE:
case CREATED:
case EARLY_FINISH:
case EARLY_START:
case FINISH:
case FINISH1:
case FINISH2:
case FINISH3:
case FINISH4:
case FINISH5:
case LATE_FINISH:
case LATE_START:
case RESUME:
case START:
case START1:
case START2:
case START3:
case START4:
case START5:
case STOP:
{
try
{
task.set(taskField, m_formats.getDateTimeFormat().parse(field));
}
catch (ParseException ex)
{
throw new MPXJException("Failed to parse date time", ex);
}
break;
}
case CONFIRMED:
case CRITICAL:
case FLAG1:
case FLAG2:
case FLAG3:
case FLAG4:
case FLAG5:
case FLAG6:
case FLAG7:
case FLAG8:
case FLAG9:
case FLAG10:
case HIDE_BAR:
case LINKED_FIELDS:
case MARKED:
case MILESTONE:
case ROLLUP:
case SUMMARY:
case UPDATE_NEEDED:
{
task.set(taskField, ((field.equalsIgnoreCase(falseText) == true) ? Boolean.FALSE : Boolean.TRUE));
break;
}
case CONSTRAINT_TYPE:
{
task.set(taskField, ConstraintTypeUtility.getInstance(m_locale, field));
break;
}
case OBJECTS:
case OUTLINE_LEVEL:
{
task.set(taskField, Integer.valueOf(field));
break;
}
case ID:
{
task.setID(Integer.valueOf(field));
break;
}
case UNIQUE_ID:
{
task.setUniqueID(Integer.valueOf(field));
break;
}
case NUMBER1:
case NUMBER2:
case NUMBER3:
case NUMBER4:
case NUMBER5:
{
try
{
task.set(taskField, m_formats.getDecimalFormat().parse(field));
}
catch (ParseException ex)
{
throw new MPXJException("Failed to parse number", ex);
}
break;
}
case PRIORITY:
{
task.set(taskField, PriorityUtility.getInstance(m_locale, field));
break;
}
case TYPE:
{
boolean fixed = !field.equalsIgnoreCase(falseText);
task.setType(fixed ? TaskType.FIXED_DURATION : TaskType.FIXED_UNITS);
break;
}
default:
{
task.set(taskField, field);
break;
}
}
}
if (m_projectConfig.getAutoWBS() == true)
{
task.generateWBS(null);
}
if (m_projectConfig.getAutoOutlineNumber() == true)
{
task.generateOutlineNumber(null);
}
if (m_projectConfig.getAutoOutlineLevel() == true)
{
task.setOutlineLevel(Integer.valueOf(1));
}
if (m_projectConfig.getAutoTaskUniqueID() == true)
{
task.setUniqueID(Integer.valueOf(m_projectConfig.getNextTaskUniqueID()));
}
if (task.getID() == null || m_projectConfig.getAutoTaskID() == true)
{
task.setID(Integer.valueOf(m_projectConfig.getNextTaskID()));
}
//
// Handle malformed MPX files - ensure we have a unique ID
//
if (task.getUniqueID() == null)
{
task.setUniqueID(task.getID());
}
}
/**
* Populates a recurring task.
*
* @param record MPX record
* @param task recurring task
* @throws MPXJException
*/
private void populateRecurringTask(Record record, RecurringTask task) throws MPXJException
{
//System.out.println(record);
task.setStartDate(record.getDateTime(1));
task.setFinishDate(record.getDateTime(2));
task.setDuration(RecurrenceUtility.getDuration(m_projectFile.getProjectProperties(), record.getInteger(3), record.getInteger(4)));
task.setOccurrences(record.getInteger(5));
task.setRecurrenceType(RecurrenceUtility.getRecurrenceType(record.getInteger(6)));
task.setUseEndDate(NumberHelper.getInt(record.getInteger(8)) == 1);
task.setDailyWorkday(NumberHelper.getInt(record.getInteger(9)) == 1);
task.setWeeklyDays(RecurrenceUtility.getDays(record.getString(10)));
task.setMonthlyRelative(NumberHelper.getInt(record.getInteger(11)) == 1);
task.setYearlyAbsolute(NumberHelper.getInt(record.getInteger(12)) == 1);
task.setDailyFrequency(record.getInteger(13));
task.setWeeklyFrequency(record.getInteger(14));
task.setMonthlyRelativeOrdinal(record.getInteger(15));
task.setMonthlyRelativeDay(RecurrenceUtility.getDay(record.getInteger(16)));
task.setMonthlyRelativeFrequency(record.getInteger(17));
task.setMonthlyAbsoluteDay(record.getInteger(18));
task.setMonthlyAbsoluteFrequency(record.getInteger(19));
task.setYearlyRelativeOrdinal(record.getInteger(20));
task.setYearlyRelativeDay(RecurrenceUtility.getDay(record.getInteger(21)));
task.setYearlyRelativeMonth(record.getInteger(22));
task.setYearlyAbsoluteDate(record.getDateTime(23));
//System.out.println(task);
}
/**
* Populate a resource assignment.
*
* @param record MPX record
* @param assignment resource assignment
* @throws MPXJException
*/
private void populateResourceAssignment(Record record, ResourceAssignment assignment) throws MPXJException
{
//
// Handle malformed MPX files - ensure that we can locate the resource
// using either the Unique ID attribute or the ID attribute.
//
Resource resource = m_projectFile.getResourceByUniqueID(record.getInteger(12));
if (resource == null)
{
resource = m_projectFile.getResourceByID(record.getInteger(0));
}
assignment.setUnits(record.getUnits(1));
assignment.setWork(record.getDuration(2));
assignment.setBaselineWork(record.getDuration(3));
assignment.setActualWork(record.getDuration(4));
assignment.setOvertimeWork(record.getDuration(5));
assignment.setCost(record.getCurrency(6));
assignment.setBaselineCost(record.getCurrency(7));
assignment.setActualCost(record.getCurrency(8));
assignment.setStart(record.getDateTime(9));
assignment.setFinish(record.getDateTime(10));
assignment.setDelay(record.getDuration(11));
//
// Calculate the remaining work
//
Duration work = assignment.getWork();
Duration actualWork = assignment.getActualWork();
if (work != null && actualWork != null)
{
if (work.getUnits() != actualWork.getUnits())
{
actualWork = actualWork.convertUnits(work.getUnits(), m_projectFile.getProjectProperties());
}
assignment.setRemainingWork(Duration.getInstance(work.getDuration() - actualWork.getDuration(), work.getUnits()));
}
if (resource != null)
{
assignment.setResourceUniqueID(resource.getUniqueID());
resource.addResourceAssignment(assignment);
}
m_eventManager.fireAssignmentReadEvent(assignment);
}
/**
* Populate a resource assignment workgroup instance.
*
* @param record MPX record
* @param workgroup workgroup instance
* @throws MPXJException
*/
private void populateResourceAssignmentWorkgroupFields(Record record, ResourceAssignmentWorkgroupFields workgroup) throws MPXJException
{
workgroup.setMessageUniqueID(record.getString(0));
workgroup.setConfirmed(NumberHelper.getInt(record.getInteger(1)) == 1);
workgroup.setResponsePending(NumberHelper.getInt(record.getInteger(1)) == 1);
workgroup.setUpdateStart(record.getDateTime(3));
workgroup.setUpdateFinish(record.getDateTime(4));
workgroup.setScheduleID(record.getString(5));
}
/**
* Populate a file creation record.
*
* @param record MPX record
* @param properties project properties
*/
static void populateFileCreationRecord(Record record, ProjectProperties properties)
{
properties.setMpxProgramName(record.getString(0));
properties.setMpxFileVersion(FileVersion.getInstance(record.getString(1)));
properties.setMpxCodePage(record.getCodePage(2));
}
/**
* This method returns the locale used by this MPX file.
*
* @return current locale
*/
public Locale getLocale()
{
return (m_locale);
}
/**
* This method sets the locale to be used by this MPX file.
*
* @param locale locale to be used
*/
public void setLocale(Locale locale)
{
m_locale = locale;
}
/**
* Retrieves an array of locales supported by this class.
*
* @return array of supported locales
*/
public Locale[] getSupportedLocales()
{
return (LocaleUtility.getSupportedLocales());
}
/**
* This method sets the flag indicating that the text version of the
* Task and Resource Table Definition records should be ignored. Ignoring
* these records gets around the problem where MPX files have been generated
* with incorrect task or resource field names, but correct task or resource
* field numbers in the numeric version of the record.
*
* @param flag Boolean flag
*/
public void setIgnoreTextModels(boolean flag)
{
m_ignoreTextModels = flag;
}
/**
* Retrieves the flag indicating that the text version of the Task and
* Resource Table Definition records should be ignored.
*
* @return Boolean flag
*/
public boolean getIgnoreTextModels()
{
return (m_ignoreTextModels);
}
private Locale m_locale = Locale.ENGLISH;
private boolean m_ignoreTextModels = true;
/**
* Transient working data.
*/
/**
* This class is used to collect relationship data awaiting
* deferred processing. We do this to allow forward references
* between tasks.
*/
protected static class DeferredRelationship
{
/**
* Retrieve the parent task.
*
* @return parent Task instance
*/
public Task getTask()
{
return m_task;
}
/**
* Set the parent task instance.
*
* @param task parent Task instance
*/
public void setTask(Task task)
{
m_task = task;
}
/**
* Retrieve the target task field.
*
* @return TaskField instance
*/
public TaskField getField()
{
return m_field;
}
/**
* Set the target task field.
*
* @param field TaskField instance
*/
public void setField(TaskField field)
{
m_field = field;
}
/**
* Retrieve the relationship data.
*
* @return relationship data
*/
public String getData()
{
return m_data;
}
/**
* Set the relationship data.
*
* @param data relationship data
*/
public void setData(String data)
{
m_data = data;
}
private Task m_task;
private TaskField m_field;
private String m_data;
}
private ProjectFile m_projectFile;
private EventManager m_eventManager;
private ProjectConfig m_projectConfig;
private Task m_lastTask;
private Resource m_lastResource;
private ProjectCalendar m_lastResourceCalendar;
private ResourceAssignment m_lastResourceAssignment;
private ProjectCalendar m_lastBaseCalendar;
private boolean m_resourceTableDefinition;
private boolean m_taskTableDefinition;
private TaskModel m_taskModel;
private ResourceModel m_resourceModel;
private char m_delimiter;
private MPXJFormats m_formats;
private List<DeferredRelationship> m_deferredRelationships;
private List<ProjectListener> m_projectListeners;
/**
* This member data is used to hold the outline level number of the
* first outline level used in the MPX file. When data from
* Microsoft Project is saved in MPX format, MSP creates an invisible
* task with an outline level as zero, which acts as an umbrella
* task for all of the other tasks defined in the file. This is not
* a strict requirement, and an MPX file could be generated from another
* source that only contains "visible" tasks that have outline levels
* >= 1.
*/
private int m_baseOutlineLevel;
}