From 39dc7555bc1c4b5d7ae6b9ee9e65809e255caed3 Mon Sep 17 00:00:00 2001 From: Jonathan Putney Date: Wed, 12 Aug 2026 09:19:58 -0400 Subject: [PATCH] Fix SCORM 2004 global objective parsing --- .../scorm2004/SequencingUsageDetector.java | 3 +- .../ims/cp/Scorm2004Organization.java | 15 ++++++-- .../parser/parsers/Scorm2004Parser.java | 15 ++++++-- .../scorm2004/Scorm2004ManifestTest.java | 34 +++++++++++++++++++ .../parser/parsers/Scorm2004ParserTest.java | 11 ++++++ 5 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/main/java/dev/jcputney/elearning/parser/input/scorm2004/SequencingUsageDetector.java b/src/main/java/dev/jcputney/elearning/parser/input/scorm2004/SequencingUsageDetector.java index eba183b3..f32db1a9 100644 --- a/src/main/java/dev/jcputney/elearning/parser/input/scorm2004/SequencingUsageDetector.java +++ b/src/main/java/dev/jcputney/elearning/parser/input/scorm2004/SequencingUsageDetector.java @@ -185,7 +185,8 @@ private static void inspectOrganizations(Scorm2004Organizations organizations, indicators.add(SequencingIndicator.ORGANIZATION_SEQUENCING); inspectSequencing(organization.getSequencing(), state, indicators, true, false); } - if (organization.isObjectivesGlobalToSystem()) { + if (organization.isObjectivesGlobalToSystemSpecified() + && organization.isObjectivesGlobalToSystem()) { indicators.add(SequencingIndicator.ORGANIZATION_OBJECTIVES_GLOBAL); state.organizationObjectivesGlobal = true; } diff --git a/src/main/java/dev/jcputney/elearning/parser/input/scorm2004/ims/cp/Scorm2004Organization.java b/src/main/java/dev/jcputney/elearning/parser/input/scorm2004/ims/cp/Scorm2004Organization.java index e0f08824..43fbd978 100644 --- a/src/main/java/dev/jcputney/elearning/parser/input/scorm2004/ims/cp/Scorm2004Organization.java +++ b/src/main/java/dev/jcputney/elearning/parser/input/scorm2004/ims/cp/Scorm2004Organization.java @@ -11,6 +11,7 @@ package dev.jcputney.elearning.parser.input.scorm2004.ims.cp; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -75,7 +76,7 @@ public final class Scorm2004Organization implements Serializable { */ @JacksonXmlProperty(isAttribute = true, localName = "objectivesGlobalToSystem", namespace = ADLSeq.NAMESPACE_URI) @JsonProperty("objectivesGlobalToSystem") - private boolean objectivesGlobalToSystem = false; + private Boolean objectivesGlobalToSystem; /** * The default for the adlcp:sharedDataGlobalToSystem attribute for items in this organization. If @@ -186,7 +187,17 @@ public void setItems(List items) { * @return true if the objectives are global to the system, false otherwise */ public boolean isObjectivesGlobalToSystem() { - return this.objectivesGlobalToSystem; + return this.objectivesGlobalToSystem == null || this.objectivesGlobalToSystem; + } + + /** + * Returns whether the manifest explicitly declared objectivesGlobalToSystem. + * + * @return true when the attribute was present in the organization element + */ + @JsonIgnore + public boolean isObjectivesGlobalToSystemSpecified() { + return this.objectivesGlobalToSystem != null; } /** diff --git a/src/main/java/dev/jcputney/elearning/parser/parsers/Scorm2004Parser.java b/src/main/java/dev/jcputney/elearning/parser/parsers/Scorm2004Parser.java index ddf6227b..d84938dd 100644 --- a/src/main/java/dev/jcputney/elearning/parser/parsers/Scorm2004Parser.java +++ b/src/main/java/dev/jcputney/elearning/parser/parsers/Scorm2004Parser.java @@ -17,6 +17,7 @@ import dev.jcputney.elearning.parser.exception.ManifestParseException; import dev.jcputney.elearning.parser.exception.ModuleException; import dev.jcputney.elearning.parser.exception.ModuleParsingException; +import dev.jcputney.elearning.parser.input.common.serialization.NormalizedIdDeserializer; import dev.jcputney.elearning.parser.input.common.serialization.Scorm2004SchemaValidator; import dev.jcputney.elearning.parser.input.scorm2004.ADLSeq; import dev.jcputney.elearning.parser.input.scorm2004.IMSSS; @@ -482,7 +483,8 @@ private Scorm2004Objective parseImsObjective(Element objectiveElement) { private Scorm2004ObjectiveMapping parseImsMapInfo(Element mapInfoElement) { Scorm2004ObjectiveMapping mapping = new Scorm2004ObjectiveMapping(); - mapping.setTargetObjectiveID(optionalAttribute(mapInfoElement, "targetObjectiveID")); + mapping.setTargetObjectiveID(normalizeTargetObjectiveId( + optionalAttribute(mapInfoElement, "targetObjectiveID"))); setBooleanAttribute(mapInfoElement, "readSatisfiedStatus", mapping::setReadSatisfiedStatus); setBooleanAttribute(mapInfoElement, "readNormalizedMeasure", mapping::setReadNormalizedMeasure); @@ -529,7 +531,8 @@ private ADLObjective parseAdlObjective(Element objectiveElement) { private MapInfo parseAdlMapInfo(Element mapInfoElement) { MapInfo mapInfo = new MapInfo(); - mapInfo.setTargetObjectiveID(optionalAttribute(mapInfoElement, "targetObjectiveID")); + mapInfo.setTargetObjectiveID(normalizeTargetObjectiveId( + optionalAttribute(mapInfoElement, "targetObjectiveID"))); setBooleanAttribute(mapInfoElement, "readRawScore", mapInfo::setReadRawScore); setBooleanAttribute(mapInfoElement, "readMinScore", mapInfo::setReadMinScore); setBooleanAttribute(mapInfoElement, "readMaxScore", mapInfo::setReadMaxScore); @@ -547,6 +550,14 @@ private MapInfo parseAdlMapInfo(Element mapInfoElement) { return mapInfo; } + private String normalizeTargetObjectiveId(String value) { + if (value == null) { + return null; + } + String decodedWhitespace = value.replaceAll("(?i)%(?:20|09|0A|0D)", " "); + return NormalizedIdDeserializer.normalize(decodedWhitespace); + } + private void setBooleanAttribute(Element element, String attributeName, Consumer setter) { String value = optionalAttribute(element, attributeName); diff --git a/src/test/java/dev/jcputney/elearning/parser/input/scorm2004/Scorm2004ManifestTest.java b/src/test/java/dev/jcputney/elearning/parser/input/scorm2004/Scorm2004ManifestTest.java index 932bbf24..b788bbba 100644 --- a/src/test/java/dev/jcputney/elearning/parser/input/scorm2004/Scorm2004ManifestTest.java +++ b/src/test/java/dev/jcputney/elearning/parser/input/scorm2004/Scorm2004ManifestTest.java @@ -110,6 +110,40 @@ void testUsesSequencingWithoutIndicators() { assertFalse(manifest.usesSequencing(), "Empty manifest should not use sequencing"); } + @Test + void objectivesGlobalToSystemDefaultsToTrueWhenOmitted() + throws IOException, XMLStreamException, ModuleParsingException, ManifestParseException { + String modulePath = + "src/test/resources/modules/scorm2004/ContentPackagingMetadata_SCORM20043rdEdition"; + Scorm2004Manifest manifest = new Scorm2004Parser(new LocalFileAccess(modulePath)) + .parseManifest(Scorm2004Parser.MANIFEST_FILE); + + Scorm2004Organization organization = manifest + .getOrganizations() + .getOrganizationList() + .get(0); + + assertTrue(organization.isObjectivesGlobalToSystem()); + assertFalse(organization.isObjectivesGlobalToSystemSpecified()); + } + + @Test + void objectivesGlobalToSystemPreservesExplicitFalse() + throws IOException, XMLStreamException, ModuleParsingException, ManifestParseException { + String modulePath = + "src/test/resources/modules/scorm2004/SequencingSimpleRemediation_SCORM20043rdEdition"; + Scorm2004Manifest manifest = new Scorm2004Parser(new LocalFileAccess(modulePath)) + .parseManifest(Scorm2004Parser.MANIFEST_FILE); + + Scorm2004Organization organization = manifest + .getOrganizations() + .getOrganizationList() + .get(0); + + assertFalse(organization.isObjectivesGlobalToSystem()); + assertTrue(organization.isObjectivesGlobalToSystemSpecified()); + } + /** * Tests the getGlobalObjectiveIds method with a manifest that contains global objectives. */ diff --git a/src/test/java/dev/jcputney/elearning/parser/parsers/Scorm2004ParserTest.java b/src/test/java/dev/jcputney/elearning/parser/parsers/Scorm2004ParserTest.java index 75fdaeff..088cdc3a 100644 --- a/src/test/java/dev/jcputney/elearning/parser/parsers/Scorm2004ParserTest.java +++ b/src/test/java/dev/jcputney/elearning/parser/parsers/Scorm2004ParserTest.java @@ -86,4 +86,15 @@ void testNormalScorm2004ManifestParsesSuccessfully() throws Exception { assertThat(result.metadata()).isNotNull(); assertThat(result.metadata().getTitle()).isNotBlank(); } + + @Test + void normalizesEncodedWhitespaceInGlobalObjectiveIds() throws Exception { + LocalFileAccess fileAccess = new LocalFileAccess( + "src/test/resources/modules/conformance/scorm2004/adl-cts/LMSTestPackage_OB-02b"); + ModuleParserFactory factory = new DefaultModuleParserFactory(fileAccess); + + Scorm2004Metadata metadata = (Scorm2004Metadata) factory.parseModule(); + + assertThat(metadata.getGlobalObjectiveIds()).containsExactly("gObj - OB 02 b"); + } }