diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1cf331842f..1aea9de91d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/maven/src/changes/changes.xml b/maven/src/changes/changes.xml
index 3be8924ce0..dac4117de8 100644
--- a/maven/src/changes/changes.xml
+++ b/maven/src/changes/changes.xml
@@ -9,6 +9,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.
Add support for reading the resource "generic" attribute from MPP files.
diff --git a/src/net/sf/mpxj/primavera/ActivitySorter.java b/src/net/sf/mpxj/primavera/ActivitySorter.java
new file mode 100644
index 0000000000..373c201edb
--- /dev/null
+++ b/src/net/sf/mpxj/primavera/ActivitySorter.java
@@ -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 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 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()
+ {
+ @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 m_wbsTasks;
+}
diff --git a/src/net/sf/mpxj/primavera/PrimaveraPMFileReader.java b/src/net/sf/mpxj/primavera/PrimaveraPMFileReader.java
index 8976dbc17b..72739b0b05 100644
--- a/src/net/sf/mpxj/primavera/PrimaveraPMFileReader.java
+++ b/src/net/sf/mpxj/primavera/PrimaveraPMFileReader.java
@@ -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;
@@ -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;
@@ -421,17 +419,20 @@ private void processTasks(ProjectType project)
{
List wbs = project.getWBS();
List tasks = project.getActivity();
-
Set uniqueIDs = new HashSet();
+ Set wbsTasks = new HashSet();
//
// 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());
@@ -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 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()
- {
- @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
@@ -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();
}
diff --git a/src/net/sf/mpxj/primavera/PrimaveraReader.java b/src/net/sf/mpxj/primavera/PrimaveraReader.java
index 005046fb5b..fa14e88165 100644
--- a/src/net/sf/mpxj/primavera/PrimaveraReader.java
+++ b/src/net/sf/mpxj/primavera/PrimaveraReader.java
@@ -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;
@@ -578,6 +577,7 @@ public void processTasks(List wbs, List tasks, List udfVals)
ProjectProperties projectProperties = m_project.getProjectProperties();
String projectName = projectProperties.getName();
Set uniqueIDs = new HashSet();
+ Set wbsTasks = new HashSet();
//
// We set the project name when we read the project properties, but that's just
@@ -602,6 +602,7 @@ public void processTasks(List wbs, List tasks, List 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);
}
@@ -700,7 +701,8 @@ public void processTasks(List wbs, List tasks, List udfVals)
m_eventManager.fireTaskReadEvent(task);
}
- sortActivities(activityIDField, m_project);
+ new ActivitySorter(TaskField.TEXT1, wbsTasks).sort(m_project);
+
updateStructure();
updateDates();
updateWork();
@@ -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 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()
- {
- @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.
diff --git a/src/net/sf/mpxj/primavera/PrimaveraXERFileReader.java b/src/net/sf/mpxj/primavera/PrimaveraXERFileReader.java
index f6cc737cf0..c5c680b2dc 100644
--- a/src/net/sf/mpxj/primavera/PrimaveraXERFileReader.java
+++ b/src/net/sf/mpxj/primavera/PrimaveraXERFileReader.java
@@ -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();
}
diff --git a/src/net/sf/mpxj/primavera/WbsRowComparatorPMXML.java b/src/net/sf/mpxj/primavera/WbsRowComparatorPMXML.java
new file mode 100644
index 0000000000..a17c69e9d3
--- /dev/null
+++ b/src/net/sf/mpxj/primavera/WbsRowComparatorPMXML.java
@@ -0,0 +1,53 @@
+/*
+ * file: WbsRowComparatorPMXML.java
+ * author: Jon Iles
+ * copyright: (c) Packwood Software 2011
+ * date: 24/11/2011
+ */
+
+/*
+ * 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.Comparator;
+
+import net.sf.mpxj.common.NumberHelper;
+import net.sf.mpxj.primavera.schema.WBSType;
+
+/**
+ * Comparator used to ensure that WBS elements read from an XER file
+ * are processed in the correct order.
+ */
+class WbsRowComparatorPMXML implements Comparator
+{
+ /**
+ * {@inheritDoc}
+ */
+ @Override public int compare(WBSType o1, WBSType o2)
+ {
+ Integer parent1 = o1.getParentObjectId();
+ Integer parent2 = o2.getParentObjectId();
+ int result = NumberHelper.compare(parent1, parent2);
+ if (result == 0)
+ {
+ Integer seq1 = o1.getSequenceNumber();
+ Integer seq2 = o2.getSequenceNumber();
+ result = NumberHelper.compare(seq1, seq2);
+ }
+ return result;
+ }
+}
diff --git a/src/net/sf/mpxj/primavera/WbsRowComparator.java b/src/net/sf/mpxj/primavera/WbsRowComparatorXER.java
similarity index 94%
rename from src/net/sf/mpxj/primavera/WbsRowComparator.java
rename to src/net/sf/mpxj/primavera/WbsRowComparatorXER.java
index ed918aa49c..d55908ecfb 100644
--- a/src/net/sf/mpxj/primavera/WbsRowComparator.java
+++ b/src/net/sf/mpxj/primavera/WbsRowComparatorXER.java
@@ -1,5 +1,5 @@
/*
- * file: WbsRowComparator.java
+ * file: WbsRowComparatorXER.java
* author: Jon Iles
* copyright: (c) Packwood Software 2011
* date: 24/11/2011
@@ -31,7 +31,7 @@
* Comparator used to ensure that WBS elements read from an XER file
* are processed in the correct order.
*/
-class WbsRowComparator implements Comparator
+class WbsRowComparatorXER implements Comparator
{
/**
* {@inheritDoc}