Read Microsoft Project, Primavera and other schedule files from Elixir, powered by MPXJ.

{:ok, project} = MpxjEx.read("plan.mpp")

project.properties.project_title
#=> "Office move"

for task <- project.tasks do
  {task.name, task.start, task.finish, task.percent_complete}
end

MpxjEx runs MPXJ in a JVM, has it write the schedule as JSON, and turns that into Elixir structs: tasks, resources, assignments, dependencies and calendars, with dates as NaiveDateTimes and durations as numbers in the unit you choose.

Supported formats

The format is detected from the file content. MPXJ reads MPP (Project 98 onwards), MPX, MSPDI (Project XML), MPD, Primavera XER, PMXML, P3 and SureTrak, Asta Powerproject and Easyplan, Phoenix, FastTrack, GanttProject, Planner, TurboProject, ConceptDraw, Synchro, Gantt Designer, SDEF, Sage, Project Commander, Deltek Open Plan and Edraw Project files.

MpxjEx.convert/3 writes MSPDI, MPX, XER, PMXML, Planner, SDEF and JSON.

Installation

Add mpxj_ex to your dependencies:

def deps do
  [
    {:mpxj_ex, "~> 0.1"}
  ]
end

You need a Java runtime, version 8 or newer, on the PATH or in JAVA_HOME. MPXJ itself (about 30 MB of jars) is downloaded the first time it's needed; see MpxjEx.Installer to choose the version or location. For releases and Docker images, install it at build time instead:

# config/prod.exs
config :mpxj_ex, auto_install: false, jar_dir: "/app/mpxj"
mix mpxj_ex.install --path /app/mpxj

Usage

Reading

{:ok, project} = MpxjEx.read("plan.mpp", time_units: :hours)

task = MpxjEx.Project.get_task_by_id(project, 3)
task.duration                  #=> 16.0
task.fields.baseline_cost      # every attribute MPXJ read is in :fields

MpxjEx.Project.predecessors(project, task)
MpxjEx.Project.task_resources(project, task)
MpxjEx.Project.task_tree(project)

Files holding several projects (Primavera XER and PMXML) can be read with MpxjEx.read_all/2.

Converting

:ok = MpxjEx.convert("plan.mpp", "plan.xml")              # MS Project XML
:ok = MpxjEx.convert("plan.mpp", "plan.xer")              # Primavera XER
:ok = MpxjEx.convert("plan.mpp", "p6.xml", format: :pmxml)

Many files: a long running JVM

Starting a JVM takes about half a second per call. To read many files, run an MpxjEx.Server and the JVM stays up between calls (a file then takes tens of milliseconds) and handles several at once:

children = [
  {MpxjEx.Server, name: MpxjEx.Server, pool_size: 4, max_memory: "2g"}
]

# then either pass it on each call...
MpxjEx.read("plan.mpp", server: MpxjEx.Server)

# ...or make it the default
config :mpxj_ex, server: MpxjEx.Server

Errors

Failures return {:error, %MpxjEx.Error{reason: reason}}, where reason is for example :file_not_found, :unsupported_format, :password_protected or :java_not_found. The ! variants raise it.

Deploying to production

In production you need a Java runtime in the image that runs your release, MPXJ installed at build time, and an MpxjEx.Server in your supervision tree. The deployment guide covers all of it: Docker, sizing memory and concurrency, handling untrusted uploads, configuration and troubleshooting.

examples/mpxj_service is a complete example: an HTTP API with a production Dockerfile and docker-compose.yml.

cd examples/mpxj_service
docker compose up --build -d
curl -F file=@plan.mpp http://localhost:4000/projects

Development

mix test                       # tests tagged :java are skipped without java
mix mpxj_ex.build_bridge       # after changing java/src (needs a JDK)

The test fixtures are real files from MPXJ's own test data, see test/fixtures/README.md. To publish a new version, follow RELEASING.md.

License

MpxjEx is MIT licensed. MPXJ, which it downloads and runs, is LGPL-2.1.