/* * This file is part of LibrePlan * * Copyright (C) 2012 Igalia, S.L. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.libreplan.importers; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import net.sf.mpxj.DateRange; import net.sf.mpxj.DayType; import net.sf.mpxj.Duration; import net.sf.mpxj.ProjectCalendar; import net.sf.mpxj.ProjectCalendarDateRanges; import net.sf.mpxj.ProjectCalendarException; import net.sf.mpxj.ProjectCalendarWeek; import net.sf.mpxj.ProjectFile; import net.sf.mpxj.ProjectProperties; import net.sf.mpxj.Relation; import net.sf.mpxj.RelationType; import net.sf.mpxj.Task; import net.sf.mpxj.TimeUnit; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.joda.time.Period; import org.libreplan.importers.CalendarDayHoursDTO.CalendarDayDTO; import org.libreplan.importers.CalendarDayHoursDTO.CalendarTypeDayDTO; import org.libreplan.importers.DependencyDTO.TypeOfDependencyDTO; /** * Class that is a converter from the MPXJ format File to {@link OrderDTO}. * * @author Alba Carro PĂ©rez <alba.carro@gmail.com> * @author Vova Perebykivskyi <vova@libreplan-enterprise.com> * * @todo It last relationships. resources, calendars, hours, etc. */ public class MPXJProjectFileConverter { private static ProjectProperties properties; /** * Map between the MPXJ Task and the OrderElemenDTO or MilestoneDTO that represent it. */ private static Map<Task, IHasTaskAssociated> mapTask; /** * Converts a ProjectFile into a {@link OrderDTO}. * * This method contains a switch that is going to select the method to call for each format. * At this time it only differences between planner and project. * * @param file * ProjectFile to extract data from. * @return ImportData with the data that we want to import. */ public static OrderDTO convert(ProjectFile file, String filename) { OrderDTO importData = null; if ( FilenameUtils.getExtension(filename).equals("planner") ) importData = getImportDataFromPlanner(file, filename); else if ( FilenameUtils.getExtension(filename).equals("mpp") ) importData = getImportDataFromMPP(file, filename); return importData; } /** * Get a list of {@link CalendarDTO} from a ProjectFile. * * @param file * ProjectFile to extract data from. * @return List<CalendarDTO> List with the calendars that we want to import. */ public static List<CalendarDTO> convertCalendars(ProjectFile file) { List<CalendarDTO> calendarDTOs = new ArrayList<>(); for (ProjectCalendar projectCalendar : file.getCalendars()) { if ( StringUtils.isBlank(projectCalendar.getName()) ) { String name = "calendar-" + UUID.randomUUID(); projectCalendar.setName(name); } calendarDTOs.add(toCalendarDTO(projectCalendar)); calendarDTOs.addAll(getDerivedCalendars(projectCalendar.getDerivedCalendars())); } return calendarDTOs; } /** * Get a list of {@link CalendarDTO} from a ProjectFile. * * @param derivedProjectCalendars * ProjectCalendar to extract data from. * @return List<CalendarDTO> List with the calendars that we want to import. */ public static List<CalendarDTO> getDerivedCalendars(List<ProjectCalendar> derivedProjectCalendars) { List<CalendarDTO> calendarDTOs = new ArrayList<>(); for (ProjectCalendar projectCalendar : derivedProjectCalendars) { if (projectCalendar.getResource() == null && projectCalendar.getName() != null && projectCalendar.getName().length() != 0) { calendarDTOs.add(toCalendarDTO(projectCalendar)); calendarDTOs.addAll(getDerivedCalendars(projectCalendar.getDerivedCalendars())); } } return calendarDTOs; } /** * Get {@link CalendarDTO} from a ProjectCalendar. * * @param projectCalendar * ProjectCalendar to extract data from. * @return List<CalendarDTO> List with the calendars that we want to import. */ private static CalendarDTO toCalendarDTO(ProjectCalendar projectCalendar) { CalendarDTO calendarDTO = new CalendarDTO(); calendarDTO.name = projectCalendar.getName(); if (projectCalendar.getParent() != null) { calendarDTO.parent = projectCalendar.getParent().getName(); } calendarDTO.calendarExceptions = getCalendarExceptionDTOs(projectCalendar.getCalendarExceptions()); List<ProjectCalendarWeek> workWeeks = projectCalendar.getWorkWeeks(); Collections.sort(workWeeks, new CompareMPXJProjectCalendarWeeks()); calendarDTO.calendarWeeks = getCalendarWeekDTOs(projectCalendar, workWeeks); return calendarDTO; } /** * Get a list of {@link CalendarExceptionDTO} from a list of CalendarExceptions. * * @param calendarExceptions * List of CalendarException to extract data from. * @return List<CalendarExceptionDTO> List with the CalendarExcepitions that we want to import. */ private static List<CalendarExceptionDTO> getCalendarExceptionDTOs( List<ProjectCalendarException> calendarExceptions) { List<CalendarExceptionDTO> calendarExceptionDTOs = new ArrayList<>(); for (ProjectCalendarException projectCalendarException : calendarExceptions) { calendarExceptionDTOs.addAll(toCalendarExceptionDTOs(projectCalendarException)); } return calendarExceptionDTOs; } /** * Get {@link CalendarExceptionDTO} from a ProjectCalendarException. * * @param projectCalendarException * ProjectCalendarException to extract data from. * @return List<CalendarExceptionDTO> with the calendar exceptions that we want to import. */ private static List<CalendarExceptionDTO> toCalendarExceptionDTOs(ProjectCalendarException projectCalendarException) { List<CalendarExceptionDTO> calendarExceptionDTOs = new ArrayList<>(); Date fromDate = projectCalendarException.getFromDate(); Date toDate = projectCalendarException.getToDate(); Period period = new Period(new DateTime(fromDate), new DateTime(toDate)); boolean working = projectCalendarException.getWorking(); int day = period.getDays(); Calendar cal = Calendar.getInstance(); cal.setTime(fromDate); List<Integer> duration = toHours(projectCalendarException); int hours; int minutes; if (duration != null) { hours = duration.get(0); minutes = duration.get(1); } else { if (working) { hours = 8; } else { hours = 0; } minutes = 0; } while (day > -1) { if (day==0) { calendarExceptionDTOs.add(toCalendarExceptionDTO(cal.getTime(), hours, minutes, working)); } else { calendarExceptionDTOs.add(toCalendarExceptionDTO(cal.getTime(), hours, minutes, working)); cal.add(Calendar.DAY_OF_MONTH, +1); } day--; } return calendarExceptionDTOs; } /** * Get {@link CalendarExceptionDTO} from a ProjectCalendarException. * * @param fromDate * Date with the day of the exception. * @param hours * int with the hours. * @param minutes * int with the minutes. * @param working * boolean to express it is a working exception or not * @return CalendarExceptionDTO with the calendar exceptions that we want to import. */ private static CalendarExceptionDTO toCalendarExceptionDTO(Date fromDate, int hours, int minutes, boolean working) { CalendarExceptionDTO calendarExceptionDTO = new CalendarExceptionDTO(); calendarExceptionDTO.date = fromDate; calendarExceptionDTO.hours = hours; calendarExceptionDTO.minutes = minutes; calendarExceptionDTO.working = working; return calendarExceptionDTO; } /** * Get a list of {@link CalendarWeekDTO} from a list of ProjectCalendarWeek. * * @param projectCalendar * ProjectCalendarWeek with the default data * @param workWeeks * List of ProjectCalendarWeek to extract data from.Assume that is ordered * for its DataRange start date. * @return List<CalendarDataDTO> List with the CalendarData that we want to import. */ private static List<CalendarWeekDTO> getCalendarWeekDTOs( ProjectCalendar projectCalendar, List<ProjectCalendarWeek> workWeeks) { List<CalendarWeekDTO> calendarDataDTOs = new ArrayList<>(); Date startCalendarDate; Date endCalendarDate; if (projectCalendar.getDateRange() == null) { startCalendarDate = projectCalendar.getParentFile().getProjectProperties().getStartDate(); endCalendarDate = projectCalendar.getParentFile().getProjectProperties().getFinishDate(); } else { startCalendarDate = projectCalendar.getDateRange().getStart(); endCalendarDate = projectCalendar.getDateRange().getEnd(); } if (workWeeks.isEmpty()) { calendarDataDTOs.add(toCalendarWeekDTO(projectCalendar)); } else { /* * TODO * This utility is not currently implemented in MPXJ. * This one is going to represent all the work weeks. * Including the ones with the default value that are in the middle of two. */ Date firsWorkWeekCalendarDate = workWeeks.get(0).getDateRange().getStart(); Calendar calendar1 = Calendar.getInstance(); Calendar calendar2 = Calendar.getInstance(); // If the star of the first work week is after the start of the default we have to fill the hole if (startCalendarDate.compareTo(firsWorkWeekCalendarDate) < 0) { calendar1.setTime(firsWorkWeekCalendarDate); calendar1.set(Calendar.DAY_OF_MONTH, -1); calendarDataDTOs.add(toCalendarWeekDTO(projectCalendar)); } Date endDate; Date nextStartDate; int j; for (int i = 0; i < workWeeks.size(); i++) { endDate = workWeeks.get(i).getDateRange().getEnd(); calendarDataDTOs.add(toCalendarWeekDTO(workWeeks.get(i))); j = i + 1; // If is not the last one if (j < workWeeks.size()) { nextStartDate = workWeeks.get(i + 1).getDateRange().getStart(); calendar1.setTime(endDate); calendar1.set(Calendar.DAY_OF_MONTH, +1); // If the end of the current work week is more than one day before the beginning of the next if (calendar1.getTime().compareTo(nextStartDate) < 0) { calendar2.setTime(nextStartDate); calendar1.set(Calendar.DAY_OF_MONTH, -1); // Adds a new default calendar week in the hole calendarDataDTOs.add(toCalendarWeekDTO(projectCalendar)); } } } Date endWorkWeekCalendarDate = workWeeks.get(workWeeks.size()).getDateRange().getEnd(); // If the end of the last work week is earlier than the end of the default we have to fill the hole if (endCalendarDate.compareTo(endWorkWeekCalendarDate) > 0) { calendar1.setTime(endWorkWeekCalendarDate); calendar1.set(Calendar.DAY_OF_MONTH, +1); calendarDataDTOs.add(toCalendarWeekDTO(projectCalendar)); } } return calendarDataDTOs; } /** * Get {@link CalendarWeekDTO} from a ProjectCalendarWeek. * * @param projectCalendarWeek * ProjectCalendarWeek to extract data from. * @return CalendarDataDTO with the calendar data that we want to import. */ private static CalendarWeekDTO toCalendarWeekDTO(ProjectCalendarWeek projectCalendarWeek) { CalendarWeekDTO calendarDataDTO = new CalendarWeekDTO(); if (projectCalendarWeek.getDateRange() != null) { calendarDataDTO.startDate = projectCalendarWeek.getDateRange().getStart(); calendarDataDTO.endDate = projectCalendarWeek.getDateRange().getEnd(); } else { calendarDataDTO.startDate = null; calendarDataDTO.endDate = null; } List<CalendarDayHoursDTO> calendarDaysHourDTOs = new ArrayList<>(); CalendarDayHoursDTO calendarDayHoursDTO; for (int i = 0; i < 7; i++) { calendarDayHoursDTO = new CalendarDayHoursDTO(); calendarDayHoursDTO.type = toCalendarTypeDayDTO(projectCalendarWeek.getDays()[i]); calendarDayHoursDTO.day = CalendarDayDTO.values()[i]; List<Integer> duration = toHours(projectCalendarWeek.getHours()[i]); if (duration != null) { calendarDayHoursDTO.hours = duration.get(0); calendarDayHoursDTO.minutes = duration.get(1); } else { if (calendarDayHoursDTO.type == CalendarTypeDayDTO.WORKING) { calendarDayHoursDTO.hours = 8; } else if (calendarDayHoursDTO.type == CalendarTypeDayDTO.NOT_WORKING) { calendarDayHoursDTO.hours = 0; } else if (calendarDayHoursDTO.type == CalendarTypeDayDTO.DEFAULT) { // TODO Grab the ones form default calendarDayHoursDTO.hours = 0; } calendarDayHoursDTO.minutes = 0; } calendarDaysHourDTOs.add(calendarDayHoursDTO); } calendarDataDTO.hoursPerDays = calendarDaysHourDTOs; return calendarDataDTO; } private static CalendarTypeDayDTO toCalendarTypeDayDTO(DayType dayType) { switch (dayType) { case DEFAULT: return CalendarTypeDayDTO.DEFAULT; case NON_WORKING: return CalendarTypeDayDTO.NOT_WORKING; case WORKING: return CalendarTypeDayDTO.WORKING; default: return null; } } /** * Get the number of hours of a ProjectCalendarHours. * * @param projectCalendarDateRanges * ProjectCalendarDateRanges to extract data from. * @return Integer with the total number of hours or null if the projectCalendarHours is null. */ private static List<Integer> toHours(ProjectCalendarDateRanges projectCalendarDateRanges) { if (projectCalendarDateRanges != null) { List<Integer> duration = new ArrayList<>(); int hours = 0; int minutes = 0; for (DateRange dateRange : projectCalendarDateRanges) { DateTime start = new DateTime(dateRange.getStart()); DateTime end = new DateTime(dateRange.getEnd()); Period period = new Period(start, end); int days = period.getDays(); if (period.getDays() != 0) { hours += 24 * days; } hours += period.getHours(); minutes += period.getMinutes(); } duration.add(hours); duration.add(minutes); return duration; } else { return null; } } /** * Converts a ProjectFile into a {@link OrderDTO}. * Assumes that the ProjectFile comes for a .planner file. * * @param file * ProjectFile to extract data from. * @return ImportData with the data that we want to import. */ private static OrderDTO getImportDataFromPlanner(ProjectFile file, String filename) { OrderDTO importData = new OrderDTO(); mapTask = new HashMap<>(); importData.name = filename.substring(0, filename.length() - 8/* ".planner" */); properties = file.getProjectProperties(); importData.startDate = properties.getStartDate(); importData.tasks = getImportTasks(file.getChildTasks()); importData.milestones = getImportMilestones(file.getChildTasks()); // MPXJ don't provide a deadline for the project so we take the finish date importData.deadline = properties.getFinishDate(); importData.dependencies = createDependencies(); importData.calendarName = file.getBaselineCalendar().getName(); return importData; } /** * Uses the map mapTask to create the list of {@link DependencyDTO}. * * @return List<DependencyDTO> * List with all the dependencies */ private static List<DependencyDTO> createDependencies() { List<DependencyDTO> dependencies = new ArrayList<>(); List<Relation> successors; Task task; DependencyDTO dependencyDTO; for (Map.Entry<Task, IHasTaskAssociated> mapEntry : mapTask.entrySet()) { task = mapEntry.getKey(); successors = task.getSuccessors(); if (successors != null) { for (Relation successor : successors) { dependencyDTO = new DependencyDTO(); dependencyDTO.origin = mapEntry.getValue(); dependencyDTO.destination = mapTask.get(successor.getTargetTask()); dependencyDTO.type = toDependencyDTOType(successor .getType()); dependencies.add(dependencyDTO); } } } return dependencies; } /** * Mapping between LP and MPXJ relationships. * * @param type * MPXJ RelationType to map. * @return TypeOfDependencyDTO * Type of the dependency for DependencyDTO */ private static TypeOfDependencyDTO toDependencyDTOType(RelationType type) { switch (type) { case FINISH_FINISH: return TypeOfDependencyDTO.END_END; case FINISH_START: return TypeOfDependencyDTO.END_START; case START_FINISH: return TypeOfDependencyDTO.START_END; case START_START: return TypeOfDependencyDTO.START_START; default: return null; } } /** * Converts a ProjectFile into a {@link OrderDTO}. * Assumes that the ProjectFile comes for a .mpp file. * * @param file * ProjectFile to extract data from. * @return ImportData with the data that we want to import. */ private static OrderDTO getImportDataFromMPP(ProjectFile file, String filename) { OrderDTO importData = new OrderDTO(); mapTask = new HashMap<>(); properties = file.getProjectProperties(); importData.startDate = properties.getStartDate(); // MPXJ doesn't provide a deadline for the project so we take the finish date importData.deadline = properties.getFinishDate(); for (Task task : file.getChildTasks()) { // Projects are represented as a level 0 task with all real task as its children. // Ignore all other top level tasks. // See http://mpxj.sourceforge.net/faq.html#extra-tasks-and-resources if ( !task.getChildTasks().isEmpty() ) { String name = task.getName(); if ( name != null ) { importData.name = name; } else { // Take the filename if the project name is not set importData.name = filename.substring(0, filename.length() - 4 /* ".mpp" */ ); } importData.tasks = getImportTasks(task.getChildTasks()); importData.milestones = getImportMilestones(task.getChildTasks()); importData.calendarName = file.getDefaultCalendar().getName(); break; } } importData.dependencies = createDependencies(); return importData; } /** * Converts a List of MPXJ Tasks into a List of {@link MilestoneDTO}. * * @param childTasks * List of MPXJ Tasks to extract data from. * @return List<MilestoneDTO> List of MilestoneDTO with the data that we want to * import. */ private static List<MilestoneDTO> getImportMilestones(List<Task> childTasks) { List<MilestoneDTO> milestones = new ArrayList<>(); for (Task task : childTasks) { if ( task.getMilestone() ) { MilestoneDTO milestone = getMilestoneData(task); mapTask.put(task, milestone); milestones.add(milestone); } } return milestones; } /** * Converts a MPXJ Task into a {@link MilestoneDTO}. * * @param task * MPXJ Task to extract data from. * @return MilestoneDTO MilestoneDTO with the data that we want to import. */ private static MilestoneDTO getMilestoneData(Task task) { MilestoneDTO milestone = new MilestoneDTO(); milestone.name = task.getName(); milestone.startDate = task.getStart(); toLibreplanConstraint(task); milestone.constraint = constraint; milestone.constraintDate = constraintDate; return milestone; } /** * Converts the Duration into an integer that represent hours. * * @param duration * Duration to convert. * @param properties * ProjectProperties needed to convert * @return int Integer with the rounded duration in hours */ private static int durationToIntHours(Duration duration, ProjectProperties properties) { Duration durationInHours = duration.convertUnits(TimeUnit.HOURS, properties); return (int) Math.floor(durationInHours.getDuration()); } /** * Converts a List of MPXJ Tasks into a List of {@link OrderElementDTO}. * * @param tasks * List of MPXJ Tasks to extract data from. * @return List<OrderElementDTO> List of ImportTask with the data that we want to import. */ private static List<OrderElementDTO> getImportTasks(List<Task> tasks) { List<OrderElementDTO> importTasks = new ArrayList<>(); for (Task task : tasks) { if ( !task.getMilestone() ) { OrderElementDTO importTask = getTaskData(task); importTask.children = getImportTasks(task.getChildTasks()); importTask.milestones = getImportMilestones(task.getChildTasks()); // This is because in MPXJ only MPP9 files have this attribute if ( task.getCalendar() != null ) { importTask.calendarName = task.getCalendar().getName(); } else { importTask.calendarName = null; } mapTask.put(task, importTask); importTasks.add(importTask); } } return importTasks; } /** * Converts a MPXJ Task into a {@link OrderElementDTO}. * * @param task * MPXJ Task to extract data from. * @return OrderElementDTO OrderElementDTO with the data that we want to import. */ private static OrderElementDTO getTaskData(Task task) { OrderElementDTO importTask = new OrderElementDTO(); importTask.name = task.getName(); importTask.startDate = task.getStart(); importTask.endDate = task.getFinish(); importTask.totalHours = durationToIntHours(task.getDuration(), properties); importTask.deadline = task.getDeadline(); toLibreplanConstraint(task); importTask.constraint = constraint; importTask.constraintDate = constraintDate; return importTask; } private static ConstraintDTO constraint; private static Date constraintDate; /** * Set the attributes constraint y constraintDate with the correct value. * * Because MPXJ has more types of constraints than Libreplan we had * choose to convert some of them. * * @param task * MPXJ Task to extract data from. */ private static void toLibreplanConstraint(Task task) { switch (task.getConstraintType()) { case AS_SOON_AS_POSSIBLE: constraint = ConstraintDTO.AS_SOON_AS_POSSIBLE; constraintDate = task.getConstraintDate(); return; case AS_LATE_AS_POSSIBLE: constraint = ConstraintDTO.AS_LATE_AS_POSSIBLE; constraintDate = task.getConstraintDate(); return; case MUST_START_ON: constraint = ConstraintDTO.START_IN_FIXED_DATE; constraintDate = task.getConstraintDate(); return; case MUST_FINISH_ON: constraint = ConstraintDTO.START_IN_FIXED_DATE; constraintDate = recalculateConstraintDateMin(task); return; case START_NO_EARLIER_THAN: constraint = ConstraintDTO.START_NOT_EARLIER_THAN; constraintDate = task.getConstraintDate(); return; case START_NO_LATER_THAN: constraint = ConstraintDTO.FINISH_NOT_LATER_THAN; constraintDate = recalculateConstraintDateSum(task); return; case FINISH_NO_EARLIER_THAN: constraint = ConstraintDTO.START_NOT_EARLIER_THAN; constraintDate = recalculateConstraintDateMin(task); return; case FINISH_NO_LATER_THAN: constraint = ConstraintDTO.FINISH_NOT_LATER_THAN; constraintDate = task.getConstraintDate(); return; default: break; } } /** * Get the new date based on the task date adding duration. * * @param task * MPXJ Task to extract data from. * @return Date new recalculated date */ private static Date recalculateConstraintDateSum(Task task) { return new Date(task.getConstraintDate().getTime() + (durationToIntHours(task.getDuration(), properties) * 60 * 60 * 1000)); } /** * Get the new date based on the task date and substracting duration. * * @param task * MPXJ Task to extract data from. * @return Date new recalculated date */ private static Date recalculateConstraintDateMin(Task task) { return new Date(task.getConstraintDate().getTime() - (durationToIntHours(task.getDuration(), properties) * 60 * 60 * 1000)); } }