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}