mirror of
https://github.com/hapifhir/org.hl7.fhir.core.git
synced 2025-03-02 09:29:13 +00:00
Check consistency between standards status and resource status
This commit is contained in:
parent
839deaf575
commit
ebc755bc2a
@ -469,7 +469,7 @@ public abstract class CanonicalResourceComparer extends ResourceComparer {
|
|||||||
} else {
|
} else {
|
||||||
VersionComparisonAnnotation.markChanged(r, version);
|
VersionComparisonAnnotation.markChanged(r, version);
|
||||||
match = new StructuralMatch<>(l.primitiveValue(), r.primitiveValue(), vmI(level, "Values Differ", fhirType()+"."+name));
|
match = new StructuralMatch<>(l.primitiveValue(), r.primitiveValue(), vmI(level, "Values Differ", fhirType()+"."+name));
|
||||||
if (level != IssueSeverity.NULL) {
|
if (level != IssueSeverity.NULL && res != null) {
|
||||||
res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, fhirType()+"."+name, "Values for "+name+" differ: '"+l.primitiveValue()+"' vs '"+r.primitiveValue()+"'", level));
|
res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, fhirType()+"."+name, "Values for "+name+" differ: '"+l.primitiveValue()+"' vs '"+r.primitiveValue()+"'", level));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,12 +171,9 @@ public class ProfileComparer extends CanonicalResourceComparer implements Profil
|
|||||||
StructuralMatch<ElementDefinitionNode> sm = new StructuralMatch<ElementDefinitionNode>(new ElementDefinitionNode(left, ln.current()), new ElementDefinitionNode(right, rn.current()));
|
StructuralMatch<ElementDefinitionNode> sm = new StructuralMatch<ElementDefinitionNode>(new ElementDefinitionNode(left, ln.current()), new ElementDefinitionNode(right, rn.current()));
|
||||||
compareElements(res, sm, ln.path(), null, ln, rn);
|
compareElements(res, sm, ln.path(), null, ln, rn);
|
||||||
res.combined = sm;
|
res.combined = sm;
|
||||||
}
|
ln = new DefinitionNavigator(session.getContextLeft(), left, true);
|
||||||
if (left.getType().equals(right.getType())) {
|
rn = new DefinitionNavigator(session.getContextRight(), right, true);
|
||||||
DefinitionNavigator ln = new DefinitionNavigator(session.getContextLeft(), left, true);
|
ch = compareDiff(ln.path(), null, ln, rn) || ch;
|
||||||
DefinitionNavigator rn = new DefinitionNavigator(session.getContextRight(), right, true);
|
|
||||||
StructuralMatch<ElementDefinitionNode> sm = new StructuralMatch<ElementDefinitionNode>(new ElementDefinitionNode(left, ln.current()), new ElementDefinitionNode(right, rn.current()));
|
|
||||||
ch = compareElements(res, sm, ln.path(), null, ln, rn) || ch;
|
|
||||||
// we don't preserve the differences - we only want the annotations
|
// we don't preserve the differences - we only want the annotations
|
||||||
}
|
}
|
||||||
res.updateDefinitionsState(ch);
|
res.updateDefinitionsState(ch);
|
||||||
@ -256,12 +253,12 @@ public class ProfileComparer extends CanonicalResourceComparer implements Profil
|
|||||||
|
|
||||||
}
|
}
|
||||||
subset.setMustSupport(left.current().getMustSupport() || right.current().getMustSupport());
|
subset.setMustSupport(left.current().getMustSupport() || right.current().getMustSupport());
|
||||||
def = comparePrimitivesWithTracking("mustSupport", left.current().getMustSupportElement(), right.current().getMustSupportElement(), null, IssueSeverity.INFORMATION, comp, right.current(), session.getForVersion()) || def;
|
def = comparePrimitivesWithTracking("mustSupport", left.current().getMustSupportElement(), right.current().getMustSupportElement(), null, IssueSeverity.INFORMATION, null, right.current(), session.getForVersion()) || def;
|
||||||
|
|
||||||
ElementDefinition superset = subset.copy();
|
ElementDefinition superset = subset.copy();
|
||||||
|
|
||||||
def = comparePrimitivesWithTracking("min", left.current().getMinElement(), right.current().getMinElement(), null, IssueSeverity.INFORMATION, comp, right.current(), session.getForVersion()) || def;
|
def = comparePrimitivesWithTracking("min", left.current().getMinElement(), right.current().getMinElement(), null, IssueSeverity.INFORMATION, null, right.current(), session.getForVersion()) || def;
|
||||||
def = comparePrimitivesWithTracking("max", left.current().getMaxElement(), right.current().getMaxElement(), null, IssueSeverity.INFORMATION, comp, right.current(), session.getForVersion()) || def;
|
def = comparePrimitivesWithTracking("max", left.current().getMaxElement(), right.current().getMaxElement(), null, IssueSeverity.INFORMATION, null, right.current(), session.getForVersion()) || def;
|
||||||
|
|
||||||
// compare and intersect
|
// compare and intersect
|
||||||
int leftMin = left.current().getMin();
|
int leftMin = left.current().getMin();
|
||||||
@ -354,7 +351,7 @@ public class ProfileComparer extends CanonicalResourceComparer implements Profil
|
|||||||
// return null;
|
// return null;
|
||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean compareChildren(ProfileComparison comp, StructuralMatch<ElementDefinitionNode> res, String path, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, IOException, FHIRFormatError {
|
private boolean compareChildren(ProfileComparison comp, StructuralMatch<ElementDefinitionNode> res, String path, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, IOException, FHIRFormatError {
|
||||||
boolean def = false;
|
boolean def = false;
|
||||||
@ -391,6 +388,126 @@ public class ProfileComparer extends CanonicalResourceComparer implements Profil
|
|||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private boolean compareDiff(String path, String sliceName, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, FHIRFormatError, IOException {
|
||||||
|
assert(path != null);
|
||||||
|
assert(left != null);
|
||||||
|
assert(right != null);
|
||||||
|
assert(left.path().equals(right.path()));
|
||||||
|
|
||||||
|
boolean def = false;
|
||||||
|
|
||||||
|
// not allowed to be different:
|
||||||
|
// ruleEqual(comp, res, left.current().getDefaultValue(), right.current().getDefaultValue(), "defaultValue", path);
|
||||||
|
// ruleEqual(comp, res, left.current().getMeaningWhenMissingElement(), right.current().getMeaningWhenMissingElement(), "meaningWhenMissing", path);
|
||||||
|
// ruleEqual(comp, res, left.current().getIsModifierElement(), right.current().getIsModifierElement(), "isModifier", path); - this check belongs in the core
|
||||||
|
// ruleEqual(comp, res, left.current().getIsSummaryElement(), right.current().getIsSummaryElement(), "isSummary", path); - so does this
|
||||||
|
|
||||||
|
// descriptive properties from ElementDefinition - merge them:
|
||||||
|
comparePrimitivesWithTracking("label", left.current().getLabelElement(), right.current().getLabelElement(), null, IssueSeverity.INFORMATION, null, right.current(), session.getForVersion());
|
||||||
|
def = comparePrimitivesWithTracking("short", left.current().getShortElement(), right.current().getShortElement(), null, IssueSeverity.INFORMATION, null, right.current(), session.getForVersion()) || def;
|
||||||
|
def = comparePrimitivesWithTracking("definition", left.current().getDefinitionElement(), right.current().getDefinitionElement(), null, IssueSeverity.INFORMATION, null, right.current(), session.getForVersion()) || def;
|
||||||
|
def = comparePrimitivesWithTracking("comment", left.current().getCommentElement(), right.current().getCommentElement(), null, IssueSeverity.INFORMATION, null, right.current(), session.getForVersion()) || def;
|
||||||
|
def = comparePrimitivesWithTracking("requirements", left.current().getRequirementsElement(), right.current().getRequirementsElement(), null, IssueSeverity.INFORMATION, null, right.current(), session.getForVersion()) || def;
|
||||||
|
def = comparePrimitivesWithTracking("mustSupport", left.current().getMustSupportElement(), right.current().getMustSupportElement(), null, IssueSeverity.INFORMATION, null, right.current(), session.getForVersion()) || def;
|
||||||
|
def = comparePrimitivesWithTracking("min", left.current().getMinElement(), right.current().getMinElement(), null, IssueSeverity.INFORMATION, null, right.current(), session.getForVersion()) || def;
|
||||||
|
def = comparePrimitivesWithTracking("max", left.current().getMaxElement(), right.current().getMaxElement(), null, IssueSeverity.INFORMATION, null, right.current(), session.getForVersion()) || def;
|
||||||
|
|
||||||
|
// add the children
|
||||||
|
def = compareDiffChildren(path, left, right) || def;
|
||||||
|
//
|
||||||
|
// // now process the slices
|
||||||
|
// if (left.current().hasSlicing() || right.current().hasSlicing()) {
|
||||||
|
// assert sliceName == null;
|
||||||
|
// if (isExtension(left.path()))
|
||||||
|
// return compareExtensions(outcome, path, superset, subset, left, right);
|
||||||
|
// // return true;
|
||||||
|
// else {
|
||||||
|
// ElementDefinitionSlicingComponent slicingL = left.current().getSlicing();
|
||||||
|
// ElementDefinitionSlicingComponent slicingR = right.current().getSlicing();
|
||||||
|
// // well, this is tricky. If one is sliced, and the other is not, then in general, the union just ignores the slices, and the intersection is the slices.
|
||||||
|
// if (left.current().hasSlicing() && !right.current().hasSlicing()) {
|
||||||
|
// // the super set is done. Any restrictions in the slices are irrelevant to what the super set says, except that we're going sum up the value sets if we can (for documentation purposes) (todo)
|
||||||
|
// // the minimum set is the slicing specified in the slicer
|
||||||
|
// subset.setSlicing(slicingL);
|
||||||
|
// // stick everything from the right to do with the slices to the subset
|
||||||
|
// copySlices(outcome.subset.getSnapshot().getElement(), left.getStructure().getSnapshot().getElement(), left.slices());
|
||||||
|
// } else if (!left.current().hasSlicing() && right.current().hasSlicing()) {
|
||||||
|
// // the super set is done. Any restrictions in the slices are irrelevant to what the super set says, except that we're going sum up the value sets if we can (for documentation purposes) (todo)
|
||||||
|
// // the minimum set is the slicing specified in the slicer
|
||||||
|
// subset.setSlicing(slicingR);
|
||||||
|
// // stick everything from the right to do with the slices to the subset
|
||||||
|
// copySlices(outcome.subset.getSnapshot().getElement(), right.getStructure().getSnapshot().getElement(), right.slices());
|
||||||
|
// } else if (isTypeSlicing(slicingL) || isTypeSlicing(slicingR)) {
|
||||||
|
// superset.getSlicing().setRules(SlicingRules.OPEN).setOrdered(false).addDiscriminator().setType(DiscriminatorType.TYPE).setPath("$this");
|
||||||
|
// subset.getSlicing().setRules(slicingL.getRules() == SlicingRules.CLOSED || slicingR.getRules() == SlicingRules.CLOSED ? SlicingRules.OPEN : SlicingRules.CLOSED).setOrdered(false).addDiscriminator().setType(DiscriminatorType.TYPE).setPath("$this");
|
||||||
|
//
|
||||||
|
// // the superset is the union of the types
|
||||||
|
// // the subset is the intersection of them
|
||||||
|
// List<DefinitionNavigator> handled = new ArrayList<>();
|
||||||
|
// for (DefinitionNavigator t : left.slices()) {
|
||||||
|
// DefinitionNavigator r = findMatchingSlice(right.slices(), t);
|
||||||
|
// if (r == null) {
|
||||||
|
// copySlice(outcome.superset.getSnapshot().getElement(), left.getStructure().getSnapshot().getElement(), t);
|
||||||
|
// } else {
|
||||||
|
// handled.add(r);
|
||||||
|
// ret = compareElements(outcome, path+":"+t.current().getSliceName(), t, r, t.current().getSliceName()) && ret;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// for (DefinitionNavigator t : right.slices()) {
|
||||||
|
// if (!handled.contains(t)) {
|
||||||
|
// copySlice(outcome.superset.getSnapshot().getElement(), right.getStructure().getSnapshot().getElement(), t);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } else if (slicingMatches(slicingL, slicingR)) {
|
||||||
|
// // if it's the same, we can try matching the slices - though we might have to give up without getting matches correct
|
||||||
|
// // there amy be implied consistency we can't reason about
|
||||||
|
// throw new DefinitionException("Slicing matches but is not handled yet at "+left.current().getId()+": ("+ProfileUtilities.summarizeSlicing(slicingL)+")");
|
||||||
|
// } else {
|
||||||
|
// // if the slicing is different, we can't compare them - or can we?
|
||||||
|
// throw new DefinitionException("Slicing doesn't match at "+left.current().getId()+": ("+ProfileUtilities.summarizeSlicing(slicingL)+" / "+ProfileUtilities.summarizeSlicing(slicingR)+")");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// // todo: name
|
||||||
|
// }
|
||||||
|
// return ret;
|
||||||
|
//
|
||||||
|
// // TODO Auto-generated method stub
|
||||||
|
// return null;
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private boolean compareDiffChildren(String path, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, IOException, FHIRFormatError {
|
||||||
|
boolean def = false;
|
||||||
|
|
||||||
|
List<DefinitionNavigator> lc = left.children();
|
||||||
|
List<DefinitionNavigator> rc = right.children();
|
||||||
|
// it's possible that one of these profiles walks into a data type and the other doesn't
|
||||||
|
// if it does, we have to load the children for that data into the profile that doesn't
|
||||||
|
// walk into it
|
||||||
|
if (lc.isEmpty() && !rc.isEmpty() && right.current().getType().size() == 1 && left.hasTypeChildren(right.current().getType().get(0), left.getStructure()))
|
||||||
|
lc = left.childrenFromType(right.current().getType().get(0), right.getStructure());
|
||||||
|
if (rc.isEmpty() && !lc.isEmpty() && left.current().getType().size() == 1 && right.hasTypeChildren(left.current().getType().get(0), right.getStructure()))
|
||||||
|
rc = right.childrenFromType(left.current().getType().get(0), left.getStructure());
|
||||||
|
|
||||||
|
List<DefinitionNavigator> matchR = new ArrayList<>();
|
||||||
|
for (DefinitionNavigator l : lc) {
|
||||||
|
DefinitionNavigator r = findInList(rc, l);
|
||||||
|
if (r == null) {
|
||||||
|
// todo
|
||||||
|
} else {
|
||||||
|
def = compareDiff(l.path(), null, l, r) || def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (DefinitionNavigator r : rc) {
|
||||||
|
if (!matchR.contains(r)) {
|
||||||
|
// todo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
private DefinitionNavigator findInList(List<DefinitionNavigator> rc, DefinitionNavigator l) {
|
private DefinitionNavigator findInList(List<DefinitionNavigator> rc, DefinitionNavigator l) {
|
||||||
for (DefinitionNavigator t : rc) {
|
for (DefinitionNavigator t : rc) {
|
||||||
if (tail(t.current().getPath()).equals(tail(l.current().getPath()))) {
|
if (tail(t.current().getPath()).equals(tail(l.current().getPath()))) {
|
||||||
@ -400,18 +517,18 @@ public class ProfileComparer extends CanonicalResourceComparer implements Profil
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ruleEqual(ProfileComparison comp, StructuralMatch<ElementDefinitionNode> res, DataType vLeft, DataType vRight, String name, String path) throws IOException {
|
// private void ruleEqual(ProfileComparison comp, StructuralMatch<ElementDefinitionNode> res, DataType vLeft, DataType vRight, String name, String path) throws IOException {
|
||||||
if (vLeft == null && vRight == null) {
|
// if (vLeft == null && vRight == null) {
|
||||||
// nothing
|
// // nothing
|
||||||
} else if (vLeft == null) {
|
// } else if (vLeft == null) {
|
||||||
vm(IssueSeverity.ERROR, "Added "+name, path, comp.getMessages(), res.getMessages());
|
// vm(IssueSeverity.ERROR, "Added "+name, path, comp.getMessages(), res.getMessages());
|
||||||
} else if (vRight == null) {
|
// } else if (vRight == null) {
|
||||||
vm(IssueSeverity.ERROR, "Removed "+name, path, comp.getMessages(), res.getMessages());
|
// vm(IssueSeverity.ERROR, "Removed "+name, path, comp.getMessages(), res.getMessages());
|
||||||
} else if (!Base.compareDeep(vLeft, vRight, false)) {
|
// } else if (!Base.compareDeep(vLeft, vRight, false)) {
|
||||||
vm(IssueSeverity.ERROR, name+" must be the same ("+toString(vLeft, true)+"/"+toString(vRight, false)+")", path, comp.getMessages(), res.getMessages());
|
// vm(IssueSeverity.ERROR, name+" must be the same ("+toString(vLeft, true)+"/"+toString(vRight, false)+")", path, comp.getMessages(), res.getMessages());
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private String toString(DataType val, boolean left) throws IOException {
|
private String toString(DataType val, boolean left) throws IOException {
|
||||||
if (val instanceof PrimitiveType)
|
if (val instanceof PrimitiveType)
|
||||||
return "'" + ((PrimitiveType) val).getValueAsString()+"'";
|
return "'" + ((PrimitiveType) val).getValueAsString()+"'";
|
||||||
|
@ -965,6 +965,7 @@ public class I18nConstants {
|
|||||||
public static final String MSG_DEPENDS_ON_DRAFT = "MSG_DEPENDS_ON_DRAFT";
|
public static final String MSG_DEPENDS_ON_DRAFT = "MSG_DEPENDS_ON_DRAFT";
|
||||||
public static final String MSG_DEPENDS_ON_EXTENSION = "MSG_DEPENDS_ON_EXTENSION";
|
public static final String MSG_DEPENDS_ON_EXTENSION = "MSG_DEPENDS_ON_EXTENSION";
|
||||||
public static final String MSG_DEPENDS_ON_PROFILE = "MSG_DEPENDS_ON_PROFILE";
|
public static final String MSG_DEPENDS_ON_PROFILE = "MSG_DEPENDS_ON_PROFILE";
|
||||||
|
public static final String VALIDATION_VAL_STATUS_INCONSISTENT = "VALIDATION_VAL_STATUS_INCONSISTENT";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1024,3 +1024,5 @@ MSG_DEPENDS_ON_EXPERIMENTAL = The {0} {1} is an experimental resource
|
|||||||
MSG_DEPENDS_ON_DRAFT = The {0} {1} is a draft resource
|
MSG_DEPENDS_ON_DRAFT = The {0} {1} is a draft resource
|
||||||
MSG_DEPENDS_ON_EXTENSION = extension
|
MSG_DEPENDS_ON_EXTENSION = extension
|
||||||
MSG_DEPENDS_ON_PROFILE = profile
|
MSG_DEPENDS_ON_PROFILE = profile
|
||||||
|
VALIDATION_VAL_STATUS_INCONSISTENT = The resource status ''{0}'' amd the standards status ''{1}'' are not consistent
|
||||||
|
|
@ -5139,7 +5139,14 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkSpecials(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack, boolean checkSpecials, PercentageTracker pct, ValidationMode mode) {
|
public boolean checkSpecials(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack, boolean checkSpecials, PercentageTracker pct, ValidationMode mode) {
|
||||||
// specific known special validations
|
if (VersionUtilities.getCanonicalResourceNames(context.getVersion()).contains(element.getType())) {
|
||||||
|
Base base = element.getExtensionValue(ToolingExtensions.EXT_STANDARDS_STATUS);
|
||||||
|
String standardsStatus = base != null && base.isPrimitive() ? base.primitiveValue() : null;
|
||||||
|
String status = element.getNamedChildValue("status");
|
||||||
|
if (!Utilities.noString(status) && !Utilities.noString(standardsStatus)) {
|
||||||
|
warning(errors, "2023-08-14", IssueType.BUSINESSRULE, element.line(), element.col(), stack.getLiteralPath(), statusCodesConsistent(status, standardsStatus), I18nConstants.VALIDATION_VAL_STATUS_INCONSISTENT, status, standardsStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (element.getType().equals(BUNDLE)) {
|
if (element.getType().equals(BUNDLE)) {
|
||||||
return new BundleValidator(this, serverBase).validateBundle(errors, element, stack, checkSpecials, hostContext, pct, mode);
|
return new BundleValidator(this, serverBase).validateBundle(errors, element, stack, checkSpecials, hostContext, pct, mode);
|
||||||
} else if (element.getType().equals("Observation")) {
|
} else if (element.getType().equals("Observation")) {
|
||||||
@ -5171,6 +5178,19 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean statusCodesConsistent(String status, String standardsStatus) {
|
||||||
|
switch (standardsStatus) {
|
||||||
|
case "draft": return Utilities.existsInList(status, "draft");
|
||||||
|
case "normative": return Utilities.existsInList(status, "active");
|
||||||
|
case "trial-use": return Utilities.existsInList(status, "draft", "active");
|
||||||
|
case "informative": return Utilities.existsInList(status, "draft", "active", "retired");
|
||||||
|
case "deprecated": return Utilities.existsInList(status, "retired");
|
||||||
|
case "withdrawn": return Utilities.existsInList(status, "retired");
|
||||||
|
case "external": return Utilities.existsInList(status, "draft", "active", "retired");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private ResourceValidationTracker getResourceTracker(Element element) {
|
private ResourceValidationTracker getResourceTracker(Element element) {
|
||||||
ResourceValidationTracker res = resourceTracker.get(element);
|
ResourceValidationTracker res = resourceTracker.get(element);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
|
@ -158,3 +158,19 @@ v: {
|
|||||||
"system" : "urn:ietf:bcp:13"
|
"system" : "urn:ietf:bcp:13"
|
||||||
}
|
}
|
||||||
-------------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------------
|
||||||
|
{"code" : {
|
||||||
|
"system" : "http://unstats.un.org/unsd/methods/m49/m49.htm",
|
||||||
|
"code" : "001"
|
||||||
|
}, "url": "http://hl7.org/fhir/ValueSet/jurisdiction", "version": "5.0.0", "langs":"[en]", "useServer":"true", "useClient":"true", "guessSystem":"false", "valueSetMode":"NO_MEMBERSHIP_CHECK", "displayWarningMode":"false", "versionFlexible":"false", "profile": {
|
||||||
|
"resourceType" : "Parameters",
|
||||||
|
"parameter" : [{
|
||||||
|
"name" : "profile-url",
|
||||||
|
"valueString" : "http://hl7.org/fhir/ExpansionProfile/dc8fd4bc-091a-424a-8a3b-6198ef146891"
|
||||||
|
}]
|
||||||
|
}}####
|
||||||
|
v: {
|
||||||
|
"display" : "World",
|
||||||
|
"code" : "001",
|
||||||
|
"system" : "http://unstats.un.org/unsd/methods/m49/m49.htm"
|
||||||
|
}
|
||||||
|
-------------------------------------------------------------------------------------
|
||||||
|
Loading…
x
Reference in New Issue
Block a user