Skip to content

Commit

Permalink
Correctly sort Primavera schedules
Browse files Browse the repository at this point in the history
  • Loading branch information
joniles committed Jun 6, 2018
1 parent 50e8979 commit 1104c88
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 131 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Improve handling of calendar exceptions in MPX files.
* Improve handling of MPP files with large numbers of null tasks.
* Improve robustness when reading timephased data.
* Correctly sort Primavera schedules containing WBS entries with no child activities.

## 7.4.3 (25/05/2018)
* Add support for reading the resource "generic" attribute from MPP files.
Expand Down
1 change: 1 addition & 0 deletions maven/src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<action dev="joniles" type="update">Improve handling of calendar exceptions in MPX files.</action>
<action dev="joniles" type="update">Improve handling of MPP files with large numbers of null tasks.</action>
<action dev="joniles" type="update">Improve robustness when reading timephased data.</action>
<action dev="joniles" type="update">Correctly sort Primavera schedules containing WBS entries with no child activities.</action>
</release>
<release date="25/05/2018" version="7.4.3">
<action dev="joniles" type="add">Add support for reading the resource "generic" attribute from MPP files.</action>
Expand Down
114 changes: 114 additions & 0 deletions src/net/sf/mpxj/primavera/ActivitySorter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* file: ActivitySorter.java
* author: Jon Iles
* copyright: (c) Packwood Software 2018
* date: 06/06/2018
*/

/*
* 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.primavera;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;

import net.sf.mpxj.ChildTaskContainer;
import net.sf.mpxj.FieldType;
import net.sf.mpxj.Task;

/**
* Ensures correct activity order within.
*/
class ActivitySorter
{
/**
* Constructor.
*
* @param activityIDField field containing the Activity ID attribute
* @param wbsTasks set of WBS tasks
*/
public ActivitySorter(FieldType activityIDField, Set<Task> wbsTasks)
{
m_activityIDField = activityIDField;
m_wbsTasks = wbsTasks;
}

/**
* Recursively sort the supplied child tasks.
*
* @param container child tasks
*/
public void sort(ChildTaskContainer container)
{
// Do we have any tasks?
List<Task> tasks = container.getChildTasks();
if (!tasks.isEmpty())
{
for (Task task : tasks)
{
//
// Sort child activities
//
sort(task);

//
// Sort Order:
// 1. Activities come first
// 2. WBS come last
// 3. Activities ordered by activity ID
// 4. WBS ordered by ID
//
Collections.sort(tasks, new Comparator<Task>()
{
@Override public int compare(Task t1, Task t2)
{
boolean t1IsWbs = m_wbsTasks.contains(t1);
boolean t2IsWbs = m_wbsTasks.contains(t2);

// Both are WBS
if (t1IsWbs && t2IsWbs)
{
return t1.getID().compareTo(t2.getID());
}

// Both are activities
if (!t1IsWbs && !t2IsWbs)
{
String activityID1 = (String) t1.getCurrentValue(m_activityIDField);
String activityID2 = (String) t2.getCurrentValue(m_activityIDField);

if (activityID1 == null || activityID2 == null)
{
return (activityID1 == null && activityID2 == null ? 0 : (activityID1 == null ? 1 : -1));
}

return activityID1.compareTo(activityID2);
}

// One activity one WBS
return t1IsWbs ? 1 : -1;
}
});
}
}
}

final FieldType m_activityIDField;
final Set<Task> m_wbsTasks;
}
73 changes: 8 additions & 65 deletions src/net/sf/mpxj/primavera/PrimaveraPMFileReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@

import java.io.InputStream;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -47,7 +46,6 @@
import org.xml.sax.XMLReader;

import net.sf.mpxj.AssignmentField;
import net.sf.mpxj.ChildTaskContainer;
import net.sf.mpxj.ConstraintType;
import net.sf.mpxj.CustomFieldContainer;
import net.sf.mpxj.DateRange;
Expand Down Expand Up @@ -421,17 +419,20 @@ private void processTasks(ProjectType project)
{
List<WBSType> wbs = project.getWBS();
List<ActivityType> tasks = project.getActivity();

Set<Integer> uniqueIDs = new HashSet<Integer>();
Set<Task> wbsTasks = new HashSet<Task>();

//
// Read WBS entries and create tasks
//
Collections.sort(wbs, WBS_ROW_COMPARATOR);

for (WBSType row : wbs)
{
Task task = m_projectFile.addTask();
Integer uniqueID = row.getObjectId();
uniqueIDs.add(uniqueID);
wbsTasks.add(task);

task.setUniqueID(uniqueID);
task.setName(row.getName());
Expand Down Expand Up @@ -592,72 +593,12 @@ private void processTasks(ProjectType project)
m_eventManager.fireTaskReadEvent(task);
}

sortActivities(TaskField.TEXT1, m_projectFile);
new ActivitySorter(TaskField.TEXT1, wbsTasks).sort(m_projectFile);

updateStructure();
updateDates();
}

/**
* Ensure activities are sorted into Activity ID order to match Primavera.
*
* @param activityIDField field containing the Activity ID value
* @param container object containing the tasks to process
*/
private void sortActivities(final FieldType activityIDField, ChildTaskContainer container)
{
// Do we have any tasks?
List<Task> tasks = container.getChildTasks();
if (!tasks.isEmpty())
{
for (Task task : tasks)
{
//
// Sort child activities
//
sortActivities(activityIDField, task);

//
// Sort Order:
// 1. Activities come first
// 2. WBS come last
// 3. Activities ordered by activity ID
// 4. WBS ordered by ID
//
Collections.sort(tasks, new Comparator<Task>()
{
@Override public int compare(Task t1, Task t2)
{
boolean t1HasChildren = !t1.getChildTasks().isEmpty();
boolean t2HasChildren = !t2.getChildTasks().isEmpty();

// Both are WBS
if (t1HasChildren && t2HasChildren)
{
return t1.getID().compareTo(t2.getID());
}

// Both are activities
if (!t1HasChildren && !t2HasChildren)
{
String activityID1 = (String) t1.getCurrentValue(activityIDField);
String activityID2 = (String) t2.getCurrentValue(activityIDField);

if (activityID1 == null || activityID2 == null)
{
return (activityID1 == null && activityID2 == null ? 0 : (activityID1 == null ? 1 : -1));
}

return activityID1.compareTo(activityID2);
}

// One activity one WBS
return t1HasChildren ? 1 : -1;
}
});
}
}
}

/**
* The Primavera WBS entries we read in as tasks have user-entered start and end dates
* which aren't calculated or adjusted based on the child task dates. We try
Expand Down Expand Up @@ -1091,4 +1032,6 @@ private Integer mapTaskID(Integer id)
MILESTONE_MAP.put("Finish Milestone", Boolean.TRUE);
MILESTONE_MAP.put("WBS Summary", Boolean.FALSE);
}

private static final WbsRowComparatorPMXML WBS_ROW_COMPARATOR = new WbsRowComparatorPMXML();
}
67 changes: 4 additions & 63 deletions src/net/sf/mpxj/primavera/PrimaveraReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
import net.sf.mpxj.AssignmentField;
import net.sf.mpxj.Availability;
import net.sf.mpxj.AvailabilityTable;
import net.sf.mpxj.ChildTaskContainer;
import net.sf.mpxj.ConstraintType;
import net.sf.mpxj.CostRateTable;
import net.sf.mpxj.CostRateTableEntry;
Expand Down Expand Up @@ -578,6 +577,7 @@ public void processTasks(List<Row> wbs, List<Row> tasks, List<Row> udfVals)
ProjectProperties projectProperties = m_project.getProjectProperties();
String projectName = projectProperties.getName();
Set<Integer> uniqueIDs = new HashSet<Integer>();
Set<Task> wbsTasks = new HashSet<Task>();

//
// We set the project name when we read the project properties, but that's just
Expand All @@ -602,6 +602,7 @@ public void processTasks(List<Row> wbs, List<Row> tasks, List<Row> udfVals)
task.setProject(projectName); // P6 task always belongs to project
processFields(m_wbsFields, row, task);
uniqueIDs.add(task.getUniqueID());
wbsTasks.add(task);
m_eventManager.fireTaskReadEvent(task);
}

Expand Down Expand Up @@ -700,7 +701,8 @@ public void processTasks(List<Row> wbs, List<Row> tasks, List<Row> udfVals)
m_eventManager.fireTaskReadEvent(task);
}

sortActivities(activityIDField, m_project);
new ActivitySorter(TaskField.TEXT1, wbsTasks).sort(m_project);

updateStructure();
updateDates();
updateWork();
Expand Down Expand Up @@ -940,67 +942,6 @@ private void populateField(FieldContainer container, FieldType target, FieldType
container.set(target, value);
}

/**
* Ensure activities are sorted into Activity ID order to match Primavera.
*
* @param activityIDField field containing the Activity ID value
* @param container object containing the tasks to process
*/
private void sortActivities(final FieldType activityIDField, ChildTaskContainer container)
{
// Do we have any tasks?
List<Task> tasks = container.getChildTasks();
if (!tasks.isEmpty())
{
for (Task task : tasks)
{
//
// Sort child activities
//
sortActivities(activityIDField, task);

//
// Sort Order:
// 1. Activities come first
// 2. WBS come last
// 3. Activities ordered by activity ID
// 4. WBS ordered by ID
//
Collections.sort(tasks, new Comparator<Task>()
{
@Override public int compare(Task t1, Task t2)
{
boolean t1HasChildren = !t1.getChildTasks().isEmpty();
boolean t2HasChildren = !t2.getChildTasks().isEmpty();

// Both are WBS
if (t1HasChildren && t2HasChildren)
{
return t1.getID().compareTo(t2.getID());
}

// Both are activities
if (!t1HasChildren && !t2HasChildren)
{
String activityID1 = (String) t1.getCurrentValue(activityIDField);
String activityID2 = (String) t2.getCurrentValue(activityIDField);

if (activityID1 == null || activityID2 == null)
{
return (activityID1 == null && activityID2 == null ? 0 : (activityID1 == null ? 1 : -1));
}

return activityID1.compareTo(activityID2);
}

// One activity one WBS
return t1HasChildren ? 1 : -1;
}
});
}
}
}

/**
* Iterates through the tasks setting the correct
* outline level and ID values.
Expand Down
2 changes: 1 addition & 1 deletion src/net/sf/mpxj/primavera/PrimaveraXERFileReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -1027,5 +1027,5 @@ private enum XerFieldType
REQUIRED_TABLES.add("schedoptions");
}

private static final WbsRowComparator WBS_ROW_COMPARATOR = new WbsRowComparator();
private static final WbsRowComparatorXER WBS_ROW_COMPARATOR = new WbsRowComparatorXER();
}
Loading

0 comments on commit 1104c88

Please sign in to comment.