fix terminology version management and caching + better error messages for extension context

This commit is contained in:
Grahame Grieve 2021-10-19 12:47:37 +11:00
parent 0ca2a6738e
commit e21a9830b8
19 changed files with 236 additions and 82 deletions

View File

@ -4887,13 +4887,13 @@ public class ProfileUtilities extends TranslatingUtilities {
private Piece describeCoded(HierarchicalTableGenerator gen, DataType fixed) {
if (fixed instanceof Coding) {
Coding c = (Coding) fixed;
ValidationResult vr = context.validateCode(terminologyServiceOptions , c.getSystem(), c.getCode(), c.getDisplay());
ValidationResult vr = context.validateCode(terminologyServiceOptions , c.getSystem(), c.getVersion(), c.getCode(), c.getDisplay());
if (vr.getDisplay() != null)
return gen.new Piece(null, " ("+vr.getDisplay()+")", null).addStyle("color: darkgreen");
} else if (fixed instanceof CodeableConcept) {
CodeableConcept cc = (CodeableConcept) fixed;
for (Coding c : cc.getCoding()) {
ValidationResult vr = context.validateCode(terminologyServiceOptions, c.getSystem(), c.getCode(), c.getDisplay());
ValidationResult vr = context.validateCode(terminologyServiceOptions, c.getSystem(), c.getVersion(), c.getCode(), c.getDisplay());
if (vr.getDisplay() != null)
return gen.new Piece(null, " ("+vr.getDisplay()+")", null).addStyle("color: darkgreen");
}

View File

@ -726,16 +726,16 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
// --- validate code -------------------------------------------------------------------------------
@Override
public ValidationResult validateCode(ValidationOptions options, String system, String code, String display) {
public ValidationResult validateCode(ValidationOptions options, String system, String version, String code, String display) {
assert options != null;
Coding c = new Coding(system, code, display);
Coding c = new Coding(system, version, code, display);
return validateCode(options, c, null);
}
@Override
public ValidationResult validateCode(ValidationOptions options, String system, String code, String display, ValueSet vs) {
public ValidationResult validateCode(ValidationOptions options, String system, String version, String code, String display, ValueSet vs) {
assert options != null;
Coding c = new Coding(system, code, display);
Coding c = new Coding(system, version, code, display);
return validateCode(options, c, vs);
}
@ -782,9 +782,10 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
for (CodingValidationRequest t : codes) {
if (!t.hasResult()) {
String codeKey = t.getCoding().hasVersion() ? t.getCoding().getSystem()+"|"+t.getCoding().getVersion() : t.getCoding().getSystem();
if (!options.isUseServer()) {
t.setResult(new ValidationResult(IssueSeverity.WARNING,formatMessage(I18nConstants.UNABLE_TO_VALIDATE_CODE_WITHOUT_USING_SERVER), TerminologyServiceErrorClass.BLOCKED_BY_OPTIONS));
} else if (unsupportedCodeSystems.contains(t.getCoding().getSystem())) {
} else if (unsupportedCodeSystems.contains(codeKey)) {
t.setResult(new ValidationResult(IssueSeverity.ERROR,formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, t.getCoding().getSystem()), TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED));
} else if (noTerminologyServer) {
t.setResult(new ValidationResult(IssueSeverity.ERROR,formatMessage(I18nConstants.ERROR_VALIDATING_CODE_RUNNING_WITHOUT_TERMINOLOGY_SERVICES), TerminologyServiceErrorClass.NOSERVICE));
@ -882,10 +883,11 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
}
}
String codeKey = code.hasVersion() ? code.getSystem()+"|"+code.getVersion() : code.getSystem();
if (!options.isUseServer()) {
return new ValidationResult(IssueSeverity.WARNING,formatMessage(I18nConstants.UNABLE_TO_VALIDATE_CODE_WITHOUT_USING_SERVER), TerminologyServiceErrorClass.BLOCKED_BY_OPTIONS);
}
if (unsupportedCodeSystems.contains(code.getSystem())) {
if (unsupportedCodeSystems.contains(codeKey)) {
return new ValidationResult(IssueSeverity.ERROR,formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, code.getSystem()), TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
}
@ -910,8 +912,8 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
} catch (Exception e) {
res = new ValidationResult(IssueSeverity.ERROR, e.getMessage() == null ? e.getClass().getName() : e.getMessage()).setTxLink(txLog == null ? null : txLog.getLastId()).setErrorClass(TerminologyServiceErrorClass.SERVER_ERROR);
}
if (res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) {
unsupportedCodeSystems.add(code.getSystem());
if (res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED && !code.hasVersion()) {
unsupportedCodeSystems.add(codeKey);
} else if (txCache != null) { // we never cache unsuppoted code systems - we always keep trying (but only once per run)
txCache.cacheValidation(cacheToken, res, TerminologyCache.PERMANENT);
}
@ -925,6 +927,9 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
if (options.getValueSetMode() != ValueSetMode.ALL_CHECKS) {
pIn.addParameter("valueSetMode", options.getValueSetMode().toString());
}
if (options.versionFlexible()) {
pIn.addParameter("default-to-latest-version", true);
}
}
@Override
@ -1118,6 +1123,10 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte
txCache.removeCS(url);
}
public void clearTS() {
txCache.clear();
}
@Override
public List<ConceptMap> findMapsForSource(String url) throws FHIRException {

View File

@ -544,6 +544,12 @@ public interface IWorkerContext {
private TerminologyServiceErrorClass errorClass;
private String txLink;
@Override
public String toString() {
return "ValidationResult [definition=" + definition + ", system=" + system + ", severity=" + severity + ", message=" + message + ", errorClass="
+ errorClass + ", txLink=" + txLink + "]";
}
public ValidationResult(IssueSeverity severity, String message) {
this.severity = severity;
this.message = message;
@ -583,6 +589,10 @@ public interface IWorkerContext {
return definition == null ? null : definition.getCode();
}
public String getDefinition() {
return definition == null ? null : definition.getDefinition();
}
public ConceptDefinitionComponent asConceptDefinition() {
return definition;
}
@ -671,7 +681,7 @@ public interface IWorkerContext {
* @param display - equals Coding.display (optional)
* @return
*/
public ValidationResult validateCode(ValidationOptions options, String system, String code, String display);
public ValidationResult validateCode(ValidationOptions options, String system, String version, String code, String display);
/**
* Validation of a code - consult the terminology infrstructure and/or service
@ -688,7 +698,7 @@ public interface IWorkerContext {
* @param vs the applicable valueset (optional)
* @return
*/
public ValidationResult validateCode(ValidationOptions options, String system, String code, String display, ValueSet vs);
public ValidationResult validateCode(ValidationOptions options, String system, String version, String code, String display, ValueSet vs);
/**
* Validation of a code - consult the terminology infrstructure and/or service

View File

@ -124,6 +124,9 @@ public class TerminologyCache {
load();
}
public void clear() {
caches.clear();
}
public CacheToken generateValidationToken(ValidationOptions options, Coding code, ValueSet vs) {
CacheToken ct = new CacheToken();
if (code.hasSystem())
@ -337,11 +340,36 @@ public class TerminologyCache {
sw.write(" \"error\" : \""+Utilities.escapeJson(ce.e.getError()).trim()+"\"\r\n}\r\n");
} else {
sw.write("v: {\r\n");
sw.write(" \"display\" : \""+Utilities.escapeJson(ce.v.getDisplay()).trim()+"\",\r\n");
sw.write(" \"code\" : \""+Utilities.escapeJson(ce.v.getCode()).trim()+"\",\r\n");
sw.write(" \"system\" : \""+Utilities.escapeJson(ce.v.getSystem()).trim()+"\",\r\n");
sw.write(" \"severity\" : "+(ce.v.getSeverity() == null ? "null" : "\""+ce.v.getSeverity().toCode().trim()+"\"")+",\r\n");
sw.write(" \"error\" : \""+Utilities.escapeJson(ce.v.getMessage()).trim()+"\"\r\n}\r\n");
boolean first = true;
if (ce.v.getDisplay() != null) {
if (first) first = false; else sw.write(",\r\n");
sw.write(" \"display\" : \""+Utilities.escapeJson(ce.v.getDisplay()).trim()+"\"");
}
if (ce.v.getCode() != null) {
if (first) first = false; else sw.write(",\r\n");
sw.write(" \"code\" : \""+Utilities.escapeJson(ce.v.getCode()).trim()+"\"");
}
if (ce.v.getSystem() != null) {
if (first) first = false; else sw.write(",\r\n");
sw.write(" \"system\" : \""+Utilities.escapeJson(ce.v.getSystem()).trim()+"\"");
}
if (ce.v.getSeverity() != null) {
if (first) first = false; else sw.write(",\r\n");
sw.write(" \"severity\" : "+"\""+ce.v.getSeverity().toCode().trim()+"\""+"");
}
if (ce.v.getMessage() != null) {
if (first) first = false; else sw.write(",\r\n");
sw.write(" \"error\" : \""+Utilities.escapeJson(ce.v.getMessage()).trim()+"\"");
}
if (ce.v.getErrorClass() != null) {
if (first) first = false; else sw.write(",\r\n");
sw.write(" \"class\" : \""+Utilities.escapeJson(ce.v.getErrorClass().toString())+"\"");
}
if (ce.v.getDefinition() != null) {
if (first) first = false; else sw.write(",\r\n");
sw.write(" \"definition\" : \""+Utilities.escapeJson(ce.v.getDefinition()).trim()+"\"");
}
sw.write("\r\n}\r\n");
}
sw.write(ENTRY_MARKER+"\r\n");
}
@ -386,11 +414,15 @@ public class TerminologyCache {
else
ce.e = new ValueSetExpansionOutcome(error, TerminologyServiceErrorClass.UNKNOWN);
} else {
IssueSeverity severity = o.get("severity") instanceof JsonNull ? null : IssueSeverity.fromCode(o.get("severity").getAsString());
String t = loadJS(o.get("severity"));
IssueSeverity severity = t == null ? null : IssueSeverity.fromCode(t);
String display = loadJS(o.get("display"));
String code = loadJS(o.get("code"));
String system = loadJS(o.get("system"));
ce.v = new ValidationResult(severity, error, system, new ConceptDefinitionComponent().setDisplay(display).setCode(code));
String definition = loadJS(o.get("definition"));
t = loadJS(o.get("class"));
TerminologyServiceErrorClass errorClass = t == null ? null : TerminologyServiceErrorClass.valueOf(t) ;
ce.v = new ValidationResult(severity, error, system, new ConceptDefinitionComponent().setDisplay(display).setDefinition(definition).setCode(code)).setErrorClass(errorClass);
}
nc.map.put(String.valueOf(hashNWS(ce.request)), ce);
nc.list.add(ce);

View File

@ -605,6 +605,12 @@ public class Coding extends DataType implements IBaseCoding, ICompositeType, ICo
return res;
}
public Coding(String theSystem, String theVersion, String theCode, String theDisplay) {
setSystem(theSystem);
setVersion(theVersion);
setCode(theCode);
setDisplay(theDisplay);
}
// end addition
}

View File

@ -136,7 +136,7 @@ public class ConceptMapRenderer extends TerminologyRenderer {
tr = tbl.tr();
XhtmlNode td = tr.td();
td.addText(ccl.getCode());
display = getDisplayForConcept(grp.getSource(), ccl.getCode());
display = getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode());
if (display != null && !isSameCodeAndDisplay(ccl.getCode(), display))
td.tx(" ("+display+")");
TargetElementComponent ccm = ccl.getTarget().get(0);
@ -152,7 +152,7 @@ public class ConceptMapRenderer extends TerminologyRenderer {
}
td = tr.td();
td.addText(ccm.getCode());
display = getDisplayForConcept(grp.getTarget(), ccm.getCode());
display = getDisplayForConcept(systemFromCanonical(grp.getTarget()), versionFromCanonical(grp.getTarget()), ccm.getCode());
if (display != null && !isSameCodeAndDisplay(ccm.getCode(), display))
td.tx(" ("+display+")");
if (comment)
@ -217,7 +217,7 @@ public class ConceptMapRenderer extends TerminologyRenderer {
td.addText(ccl.getCode());
else
td.addText(grp.getSource()+" / "+ccl.getCode());
display = getDisplayForConcept(grp.getSource(), ccl.getCode());
display = getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode());
tr.td().style("border-left-width: 0px").tx(display == null ? "" : display);
tr.td().colspan("4").style("background-color: #efefef").tx("(not mapped)");
@ -238,7 +238,7 @@ public class ConceptMapRenderer extends TerminologyRenderer {
td.addText(ccl.getCode());
else
td.addText(grp.getSource()+" / "+ccl.getCode());
display = getDisplayForConcept(grp.getSource(), ccl.getCode());
display = getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode());
td = tr.td();
if (!first)
td.style("border-left-width: 0px; border-top-style: none");
@ -283,7 +283,7 @@ public class ConceptMapRenderer extends TerminologyRenderer {
td.addText(ccm.getCode());
else
td.addText(grp.getTarget()+" / "+ccm.getCode());
display = getDisplayForConcept(grp.getTarget(), ccm.getCode());
display = getDisplayForConcept(systemFromCanonical(grp.getTarget()), versionFromCanonical(grp.getTarget()), ccm.getCode());
tr.td().style("border-left-width: 0px").tx(display == null ? "" : display);
for (String s : targets.keySet()) {
@ -408,11 +408,9 @@ public class ConceptMapRenderer extends TerminologyRenderer {
private String getDisplay(List<OtherElementComponent> list, String s) {
for (OtherElementComponent c : list) {
if (s.equals(c.getProperty()))
return getDisplayForConcept(c.getSystem(), c.getValue());
return getDisplayForConcept(systemFromCanonical(c.getSystem()), versionFromCanonical(c.getSystem()), c.getValue());
}
return null;
}
}

View File

@ -191,8 +191,8 @@ public class DataRenderer extends Renderer {
return b.toString();
}
private String lookupCode(String system, String code) {
ValidationResult t = getContext().getWorker().validateCode(getContext().getTerminologyServiceOptions(), system, code, null);
private String lookupCode(String system, String version, String code) {
ValidationResult t = getContext().getWorker().validateCode(getContext().getTerminologyServiceOptions().setVersionFlexible(true), system, version, code, null);
if (t != null && t.getDisplay() != null)
return t.getDisplay();
@ -484,7 +484,7 @@ public class DataRenderer extends Renderer {
if (c.hasDisplayElement())
return c.getDisplay();
if (Utilities.noString(s))
s = lookupCode(c.getSystem(), c.getCode());
s = lookupCode(c.getSystem(), c.getVersion(), c.getCode());
if (Utilities.noString(s))
s = c.getCode();
return s;
@ -507,7 +507,7 @@ public class DataRenderer extends Renderer {
if (c.hasDisplayElement())
s = c.getDisplay();
if (Utilities.noString(s))
s = lookupCode(c.getSystem(), c.getCode());
s = lookupCode(c.getSystem(), c.getVersion(), c.getCode());
String sn = describeSystem(c.getSystem());
@ -539,13 +539,13 @@ public class DataRenderer extends Renderer {
if (c.hasDisplayElement())
s = c.getDisplay();
if (Utilities.noString(s))
s = lookupCode(c.getSystem(), c.getCode());
s = lookupCode(c.getSystem(), c.getVersion(), c.getCode());
if (Utilities.noString(s))
s = c.getCode();
if (showCodeDetails) {
x.addText(s+" (Details: "+TerminologyRenderer.describeSystem(c.getSystem())+" code "+c.getCode()+" = '"+lookupCode(c.getSystem(), c.getCode())+"', stated as '"+c.getDisplay()+"')");
x.addText(s+" (Details: "+TerminologyRenderer.describeSystem(c.getSystem())+" code "+c.getCode()+" = '"+lookupCode(c.getSystem(), c.getVersion(), c.getCode())+"', stated as '"+c.getDisplay()+"')");
} else
x.span(null, "{"+c.getSystem()+" "+c.getCode()+"}").addText(s);
}
@ -564,7 +564,7 @@ public class DataRenderer extends Renderer {
// still? ok, let's try looking it up
for (Coding c : cc.getCoding()) {
if (c.hasCode() && c.hasSystem()) {
s = lookupCode(c.getSystem(), c.getCode());
s = lookupCode(c.getSystem(), c.getVersion(), c.getCode());
if (!Utilities.noString(s))
break;
}
@ -611,7 +611,7 @@ public class DataRenderer extends Renderer {
// still? ok, let's try looking it up
for (Coding c : cc.getCoding()) {
if (c.hasCodeElement() && c.hasSystemElement()) {
s = lookupCode(c.getSystem(), c.getCode());
s = lookupCode(c.getSystem(), c.getVersion(), c.getCode());
if (!Utilities.noString(s))
break;
}
@ -636,7 +636,7 @@ public class DataRenderer extends Renderer {
first = false;
} else
sp.tx("; ");
sp.tx("{"+TerminologyRenderer.describeSystem(c.getSystem())+" code '"+c.getCode()+"' = '"+lookupCode(c.getSystem(), c.getCode())+(c.hasDisplay() ? "', given as '"+c.getDisplay()+"'}" : ""));
sp.tx("{"+TerminologyRenderer.describeSystem(c.getSystem())+" code '"+c.getCode()+"' = '"+lookupCode(c.getSystem(), c.getVersion(), c.getCode())+(c.hasDisplay() ? "', given as '"+c.getDisplay()+"'}" : ""));
}
sp.tx(")");
} else {
@ -661,7 +661,7 @@ public class DataRenderer extends Renderer {
else if (ii.getType().hasCoding() && ii.getType().getCoding().get(0).hasDisplay())
s = ii.getType().getCoding().get(0).getDisplay()+": "+s;
else if (ii.getType().hasCoding() && ii.getType().getCoding().get(0).hasCode())
s = lookupCode(ii.getType().getCoding().get(0).getSystem(), ii.getType().getCoding().get(0).getCode())+": "+s;
s = lookupCode(ii.getType().getCoding().get(0).getSystem(), ii.getType().getCoding().get(0).getVersion(), ii.getType().getCoding().get(0).getCode())+": "+s;
} else {
s = "id: "+s;
}
@ -838,7 +838,7 @@ public class DataRenderer extends Renderer {
s.append("(system = '").append(TerminologyRenderer.describeSystem(q.getSystem()))
.append("' code ").append(q.getCode())
.append(" = '").append(lookupCode(q.getSystem(), q.getCode())).append("')");
.append(" = '").append(lookupCode(q.getSystem(), null, q.getCode())).append("')");
return s.toString();
}
@ -858,7 +858,7 @@ public class DataRenderer extends Renderer {
else if (q.hasCode())
x.tx(" "+q.getCode());
if (showCodeDetails && q.hasCode()) {
x.span("background: LightGoldenRodYellow", null).tx(" (Details: "+TerminologyRenderer.describeSystem(q.getSystem())+" code "+q.getCode()+" = '"+lookupCode(q.getSystem(), q.getCode())+"')");
x.span("background: LightGoldenRodYellow", null).tx(" (Details: "+TerminologyRenderer.describeSystem(q.getSystem())+" code "+q.getCode()+" = '"+lookupCode(q.getSystem(), null, q.getCode())+"')");
}
}
@ -1160,4 +1160,25 @@ public class DataRenderer extends Renderer {
return b.toString();
}
protected String versionFromCanonical(String system) {
if (system == null) {
return null;
} else if (system.contains("|")) {
return system.substring(0, system.indexOf("|"));
} else {
return system;
}
}
protected String systemFromCanonical(String system) {
if (system == null) {
return null;
} else if (system.contains("|")) {
return system.substring(system.indexOf("|")+1);
} else {
return system;
}
}
}

View File

@ -309,10 +309,10 @@ public abstract class TerminologyRenderer extends ResourceRenderer {
protected String getDisplayForConcept(String system, String value) {
protected String getDisplayForConcept(String system, String version, String value) {
if (value == null || system == null)
return null;
ValidationResult cl = getContext().getWorker().validateCode(getContext().getTerminologyServiceOptions(), system, value, null);
ValidationResult cl = getContext().getWorker().validateCode(getContext().getTerminologyServiceOptions().setVersionFlexible(true), system, version, value, null);
return cl == null ? null : cl.getDisplay();
}

View File

@ -896,7 +896,7 @@ public class ValueSetRenderer extends TerminologyRenderer {
li.ah(href).addText(f.getValue());
} else if ("concept".equals(f.getProperty()) && inc.hasSystem()) {
li.addText(f.getValue());
ValidationResult vr = getContext().getWorker().validateCode(getContext().getTerminologyServiceOptions(), inc.getSystem(), f.getValue(), null);
ValidationResult vr = getContext().getWorker().validateCode(getContext().getTerminologyServiceOptions(), inc.getSystem(), inc.getVersion(), f.getValue(), null);
if (vr.isOk()) {
li.tx(" ("+vr.getDisplay()+")");
}

View File

@ -25,9 +25,11 @@ import org.hl7.fhir.exceptions.FHIRFormatError;
import org.hl7.fhir.exceptions.PathEngineException;
import org.hl7.fhir.r5.conformance.ProfileUtilities;
import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult;
import org.hl7.fhir.r5.model.Base;
import org.hl7.fhir.r5.model.BaseDateTimeType;
import org.hl7.fhir.r5.model.BooleanType;
import org.hl7.fhir.r5.model.CodeableConcept;
import org.hl7.fhir.r5.model.Constants;
import org.hl7.fhir.r5.model.DateTimeType;
import org.hl7.fhir.r5.model.DateType;
@ -54,6 +56,7 @@ import org.hl7.fhir.r5.model.TypeConvertor;
import org.hl7.fhir.r5.model.TypeDetails;
import org.hl7.fhir.r5.model.TypeDetails.ProfiledType;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.renderers.DataRenderer;
import org.hl7.fhir.r5.utils.FHIRLexer.FHIRLexerException;
import org.hl7.fhir.r5.utils.FHIRPathEngine.IEvaluationContext.FunctionDetails;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
@ -2329,7 +2332,8 @@ public class FHIRPathEngine {
private List<Base> opMemberOf(ExecutionContext context, List<Base> left, List<Base> right, ExpressionNode expr) throws FHIRException {
boolean ans = false;
ValueSet vs = hostServices != null ? hostServices.resolveValueSet(context.appInfo, right.get(0).primitiveValue()) : worker.fetchResource(ValueSet.class, right.get(0).primitiveValue());
String url = right.get(0).primitiveValue();
ValueSet vs = hostServices != null ? hostServices.resolveValueSet(context.appInfo, url) : worker.fetchResource(ValueSet.class, url);
if (vs != null) {
for (Base l : left) {
if (Utilities.existsInList(l.fhirType(), "code", "string", "uri")) {
@ -2341,7 +2345,10 @@ public class FHIRPathEngine {
ans = true;
}
} else if (l.fhirType().equals("CodeableConcept")) {
if (worker.validateCode(terminologyServiceOptions, TypeConvertor.castToCodeableConcept(l), vs).isOk()) {
CodeableConcept cc = TypeConvertor.castToCodeableConcept(l);
ValidationResult vr = worker.validateCode(terminologyServiceOptions, cc, vs);
// System.out.println("~~~ "+DataRenderer.display(worker, cc)+ " memberOf "+url+": "+vr.toString());
if (vr.isOk()) {
ans = true;
}
} else {

View File

@ -322,16 +322,9 @@ public class NPMPackageGenerator {
public void addFile(Category cat, String name, byte[] content) throws IOException {
String path = cat.getDirectory()+name;
if (!path.startsWith("package/")) {
path = "package/" +path;
}
if (path.length() > 100) {
name = name.substring(0, name.indexOf("-"))+"-"+UUID.randomUUID().toString();
path = cat.getDirectory()+name;
if (!path.startsWith("package/")) {
path = "package/" +path;
}
path = cat.getDirectory()+name;
}
if (created.contains(path)) {

View File

@ -1752,6 +1752,7 @@ public class StructureMapUtilities {
// if we can get this as a valueSet, we will
String system = null;
String display = null;
String version = null;
ValueSet vs = Utilities.noString(uri) ? null : worker.fetchResourceWithException(ValueSet.class, uri);
if (vs != null) {
ValueSetExpansionOutcome vse = worker.expandVS(vs, true, false);
@ -1763,20 +1764,23 @@ public class StructureMapUtilities {
b.append(t.getCode());
if (code.equals(t.getCode()) && t.hasSystem()) {
system = t.getSystem();
version = t.getVersion();
display = t.getDisplay();
break;
}
if (code.equalsIgnoreCase(t.getDisplay()) && t.hasSystem()) {
system = t.getSystem();
version = t.getVersion();
display = t.getDisplay();
break;
}
}
if (system == null)
throw new FHIRException("The code '" + code + "' is not in the value set '" + uri + "' (valid codes: " + b.toString() + "; also checked displays)");
} else
} else {
system = uri;
ValidationResult vr = worker.validateCode(terminologyServiceOptions, system, code, null);
}
ValidationResult vr = worker.validateCode(terminologyServiceOptions.setVersionFlexible(true), system, version, code, null);
if (vr != null && vr.getDisplay() != null)
display = vr.getDisplay();
return new Coding().setSystem(system).setCode(code).setDisplay(display);

View File

@ -105,8 +105,10 @@ public class I18nConstants {
public static final String ERROR_READING__FROM_PACKAGE__ = "Error_reading__from_package__";
public static final String ERROR_VALIDATING_CODE_RUNNING_WITHOUT_TERMINOLOGY_SERVICES = "Error_validating_code_running_without_terminology_services";
public static final String ERROR_WRITING_NUMBER__TO_JSON = "error_writing_number__to_JSON";
public static final String EXTENSION_EXT_CONTEXT_WRONG = "Extension_EXT_Context_Wrong";
public static final String EXTENSION_EXT_CONTEXT_WRONG_XVER = "EXTENSION_EXT_CONTEXT_WRONG_XVER";
public static final String EXTENSION_EXTP_CONTEXT_WRONG = "Extension_EXTP_Context_Wrong";
public static final String EXTENSION_EXTP_CONTEXT_WRONG_XVER = "EXTENSION_EXTP_CONTEXT_WRONG_XVER";
public static final String EXTENSION_EXTM_CONTEXT_WRONG = "Extension_EXTM_Context_Wrong";
public static final String EXTENSION_EXTM_CONTEXT_WRONG_XVER = "EXTENSION_EXTM_CONTEXT_WRONG_XVER";
public static final String EXTENSION_EXT_COUNT_MISMATCH = "Extension_EXT_Count_Mismatch";
public static final String EXTENSION_EXT_COUNT_NOTFOUND = "Extension_EXT_Count_NotFound";
public static final String EXTENSION_EXT_FIXED_BANNED = "Extension_EXT_Fixed_Banned";

View File

@ -12,6 +12,7 @@ public class ValidationOptions {
private boolean guessSystem = false;
private ValueSetMode valueSetMode = ValueSetMode.ALL_CHECKS;
private boolean vsAsUrl;
private boolean versionFlexible = true;
public ValidationOptions() {
super();
@ -45,6 +46,7 @@ public class ValidationOptions {
n.useClient = useClient;
n.guessSystem = guessSystem;
n.vsAsUrl = vsAsUrl;
n.versionFlexible = versionFlexible;
return n;
}
@ -75,7 +77,8 @@ public class ValidationOptions {
public String toJson() {
return "\"lang\":\""+language+"\", \"useServer\":\""+Boolean.toString(useServer)+"\", \"useClient\":\""+Boolean.toString(useClient)+"\", \"guessSystem\":\""+Boolean.toString(guessSystem)+"\", \"valueSetMode\":\""+valueSetMode.toString()+"\"";
return "\"lang\":\""+language+"\", \"useServer\":\""+Boolean.toString(useServer)+"\", \"useClient\":\""+Boolean.toString(useClient)+"\", "+
"\"guessSystem\":\""+Boolean.toString(guessSystem)+"\", \"valueSetMode\":\""+valueSetMode.toString()+"\", \"versionFlexible\":\""+Boolean.toString(versionFlexible)+"\"";
}
public static ValidationOptions defaults() {
@ -107,5 +110,15 @@ public class ValidationOptions {
return vsAsUrl;
}
public boolean versionFlexible() {
return versionFlexible;
}
public ValidationOptions setVersionFlexible(boolean value) {
ValidationOptions n = this.copy();
n.versionFlexible = value;
return n;
}
}

View File

@ -26,7 +26,8 @@ CodeSystem_CS_VS_IncludeDetails = CodeSystem {0} has an ''all system'' value set
CodeSystem_CS_VS_Invalid = CodeSystem {0} has an ''all system'' value set of {1}, but doesn''t have a single include
CODESYSTEM_CS_VS_EXP_MISMATCH = CodeSystem {0} has an ''all system'' value set of {1}, but it is an expansion with the wrong number of concepts (found {2}, expected {3})
CodeSystem_CS_VS_WrongSystem = CodeSystem {0} has an ''all system'' value set of {1}, but doesn''t have a matching system ({2})
Extension_EXT_Context_Wrong = The extension {0} is not allowed to be used at this point (allowed = {1}; this element is [{2})
Extension_EXTP_Context_Wrong = The extension {0} is not allowed to be used at this point (allowed = {1}; this element is [{2})
Extension_EXTM_Context_Wrong = The modifier extension {0} is not allowed to be used at this point (allowed = {1}; this element is [{2})
Extension_EXT_Count_Mismatch = Extensions count mismatch: expected {0} but found {1}
Extension_EXT_Count_NotFound = Extension count mismatch: unable to find extension: {0}
Extension_EXT_Fixed_Banned = No extensions allowed, as the specified fixed value doesn''t contain any extensions
@ -505,7 +506,8 @@ TYPE_SPECIFIC_CHECKS_DT_DECIMAL_CHARS = Found {0} decimal places which exceeds t
Validation_VAL_Profile_WrongType = Specified profile type was ''{0}'' in profile ''{2}'', but found type ''{1}''
Validation_VAL_Profile_WrongType2 = Type mismatch processing profile {0} at path {1}: The element type is {4}, but the profile {3} is for a different type {2}
VALIDATION_VAL_ILLEGAL_TYPE_CONSTRAINT = Illegal constraint in profile {0} at path {1} - cannot constrain to type {2} from base types {3}
EXTENSION_EXT_CONTEXT_WRONG_XVER = The extension {0} from FHIR version {3} is not allowed to be used at this point (allowed = {1}; this element is [{2}; this is a warning since contexts may be renamed between FHIR versions)
EXTENSION_EXTP_CONTEXT_WRONG_XVER = The extension {0} from FHIR version {3} is not allowed to be used at this point (allowed = {1}; this element is [{2}; this is a warning since contexts may be renamed between FHIR versions)
EXTENSION_EXTM_CONTEXT_WRONG_XVER = The modifier extension {0} from FHIR version {3} is not allowed to be used at this point (allowed = {1}; this element is [{2}; this is a warning since contexts may be renamed between FHIR versions)
SECURITY_STRING_CONTENT_ERROR = The string value contains text that looks like embedded HTML tags, which are not allowed for security reasons in this context
SECURITY_STRING_CONTENT_WARNING = The string value contains text that looks like embedded HTML tags. If this content is rendered to HTML without appropriate post-processing, it may be a security risk
ALL_OK = All OK

View File

@ -1007,4 +1007,24 @@ public class BaseValidator {
}
return null;
}
protected String versionFromCanonical(String system) {
if (system == null) {
return null;
} else if (system.contains("|")) {
return system.substring(0, system.indexOf("|"));
} else {
return system;
}
}
protected String systemFromCanonical(String system) {
if (system == null) {
return null;
} else if (system.contains("|")) {
return system.substring(system.indexOf("|")+1);
} else {
return system;
}
}
}

View File

@ -396,6 +396,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
private List<BundleValidationRule> bundleValidationRules = new ArrayList<>();
private boolean validateValueSetCodesOnTxServer = true;
private QuestionnaireMode questionnaireMode;
private ValidationOptions baseOptions = new ValidationOptions();
public InstanceValidator(IWorkerContext theContext, IEvaluationContext hostServices, XVerExtensionManager xverManager) {
super(theContext, xverManager);
@ -836,13 +837,13 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
// public API
private boolean checkCode(List<ValidationMessage> errors, Element element, String path, String code, String system, String display, boolean checkDisplay, NodeStack stack) throws TerminologyServiceException {
private boolean checkCode(List<ValidationMessage> errors, Element element, String path, String code, String system, String version, String display, boolean checkDisplay, NodeStack stack) throws TerminologyServiceException {
long t = System.nanoTime();
boolean ss = context.supportsSystem(system);
timeTracker.tx(t, "ss "+system);
if (ss) {
t = System.nanoTime();
ValidationResult s = checkCodeOnServer(stack, code, system, display, checkDisplay);
ValidationResult s = checkCodeOnServer(stack, code, system, version, display, checkDisplay);
timeTracker.tx(t, "vc "+system+"#"+code+" '"+display+"'");
if (s == null)
return true;
@ -1251,8 +1252,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
for (Coding nextCoding : cc.getCoding()) {
String nextCode = nextCoding.getCode();
String nextSystem = nextCoding.getSystem();
String nextVersion = nextCoding.getVersion();
if (isNotBlank(nextCode) && isNotBlank(nextSystem) && context.supportsSystem(nextSystem)) {
ValidationResult vr = checkCodeOnServer(stack, nextCode, nextSystem, null, false);
ValidationResult vr = checkCodeOnServer(stack, nextCode, nextSystem, nextVersion, null, false);
if (!vr.isOk()) {
txWarning(errors, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CODE_NOTVALID, nextCode, nextSystem);
}
@ -1286,12 +1288,13 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
String code = c.getCode();
String system = c.getSystem();
String display = c.getDisplay();
String version = c.getVersion();
rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, isAbsolute(system), I18nConstants.TERMINOLOGY_TX_SYSTEM_RELATIVE);
if (system != null && code != null && !noTerminologyChecks) {
rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, !isValueSet(system), I18nConstants.TERMINOLOGY_TX_SYSTEM_VALUESET2, system);
try {
if (checkCode(errors, element, path, code, system, display, checkDisplay, stack))
if (checkCode(errors, element, path, code, system, version, display, checkDisplay, stack))
if (theElementCntext != null && theElementCntext.hasBinding()) {
ElementDefinitionBindingComponent binding = theElementCntext.getBinding();
if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, I18nConstants.TERMINOLOGY_TX_BINDING_MISSING2, path)) {
@ -1473,7 +1476,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, I18nConstants.TERMINOLOGY_TX_VALUESET_NOTFOUND, describeReference(maxVSUrl))) {
try {
long t = System.nanoTime();
ValidationResult vr = checkCodeOnServer(stack, valueset, value, new ValidationOptions(stack.getWorkingLang()));
ValidationResult vr = checkCodeOnServer(stack, valueset, value, baseOptions.setLanguage(stack.getWorkingLang()));
timeTracker.tx(t, "vc "+value);
if (!vr.isOk()) {
if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure())
@ -1506,19 +1509,20 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
private void checkCoding(List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, boolean inCodeableConcept, boolean checkDisplay, NodeStack stack) {
String code = element.getNamedChildValue("code");
String system = element.getNamedChildValue("system");
String version = element.getNamedChildValue("version");
String display = element.getNamedChildValue("display");
checkCodedElement(errors, path, element, profile, theElementCntext, inCodeableConcept, checkDisplay, stack, code, system, display);
checkCodedElement(errors, path, element, profile, theElementCntext, inCodeableConcept, checkDisplay, stack, code, system, version, display);
}
private void checkCodedElement(List<ValidationMessage> errors, String path, Element element, StructureDefinition profile, ElementDefinition theElementCntext, boolean inCodeableConcept, boolean checkDisplay, NodeStack stack,
String theCode, String theSystem, String theDisplay) {
String theCode, String theSystem, String theVersion, String theDisplay) {
rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, isAbsolute(theSystem), I18nConstants.TERMINOLOGY_TX_SYSTEM_RELATIVE);
warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, Utilities.noString(theCode) || !Utilities.noString(theSystem), I18nConstants.TERMINOLOGY_TX_SYSTEM_NO_CODE);
if (theSystem != null && theCode != null && !noTerminologyChecks) {
rule(errors, IssueType.CODEINVALID, element.line(), element.col(), path, !isValueSet(theSystem), I18nConstants.TERMINOLOGY_TX_SYSTEM_VALUESET2, theSystem);
try {
if (checkCode(errors, element, path, theCode, theSystem, theDisplay, checkDisplay, stack))
if (checkCode(errors, element, path, theCode, theSystem, theVersion, theDisplay, checkDisplay, stack))
if (theElementCntext != null && theElementCntext.hasBinding()) {
ElementDefinitionBindingComponent binding = theElementCntext.getBinding();
if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, I18nConstants.TERMINOLOGY_TX_BINDING_MISSING2, path)) {
@ -1633,7 +1637,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
// two questions
// 1. can this extension be used here?
checkExtensionContext(errors, resource, container, ex, containerStack, hostContext);
checkExtensionContext(errors, resource, container, ex, containerStack, hostContext, isModifier);
if (isModifier)
rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", ex.getSnapshot().getElement().get(0).getIsModifier(), I18nConstants.EXTENSION_EXT_MODIFIER_Y, url);
@ -1697,7 +1701,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
return res;
}
private boolean checkExtensionContext(List<ValidationMessage> errors, Element resource, Element container, StructureDefinition definition, NodeStack stack, ValidatorHostContext hostContext) {
private boolean checkExtensionContext(List<ValidationMessage> errors, Element resource, Element container, StructureDefinition definition, NodeStack stack, ValidatorHostContext hostContext, boolean modifier) {
String extUrl = definition.getUrl();
boolean ok = false;
CommaSeparatedStringBuilder contexts = new CommaSeparatedStringBuilder();
@ -1774,9 +1778,11 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
if (!ok) {
if (definition.hasUserData(XVerExtensionManager.XVER_EXT_MARKER)) {
warning(errors, IssueType.STRUCTURE, container.line(), container.col(), stack.getLiteralPath(), false, I18nConstants.EXTENSION_EXT_CONTEXT_WRONG_XVER, extUrl, contexts.toString(), plist.toString());
warning(errors, IssueType.STRUCTURE, container.line(), container.col(), stack.getLiteralPath(), false,
modifier ? I18nConstants.EXTENSION_EXTM_CONTEXT_WRONG_XVER : I18nConstants.EXTENSION_EXTP_CONTEXT_WRONG_XVER, extUrl, contexts.toString(), plist.toString());
} else {
rule(errors, IssueType.STRUCTURE, container.line(), container.col(), stack.getLiteralPath(), false, I18nConstants.EXTENSION_EXT_CONTEXT_WRONG, extUrl, contexts.toString(), plist.toString());
rule(errors, IssueType.STRUCTURE, container.line(), container.col(), stack.getLiteralPath(), false,
modifier ? I18nConstants.EXTENSION_EXTP_CONTEXT_WRONG : I18nConstants.EXTENSION_EXTM_CONTEXT_WRONG, extUrl, contexts.toString(), plist.toString());
}
return false;
} else {
@ -2484,7 +2490,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
long t = System.nanoTime();
ValidationResult vr = null;
if (binding.getStrength() != BindingStrength.EXAMPLE) {
ValidationOptions options = new ValidationOptions(stack.getWorkingLang()).guessSystem();
ValidationOptions options = baseOptions.setLanguage(stack.getWorkingLang()).guessSystem();
vr = checkCodeOnServer(stack, vs, value, options);
}
timeTracker.tx(t, "vc "+value+"");
@ -2543,7 +2549,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
if (system != null || code != null ) {
checkCodedElement(theErrors, thePath, element, theProfile, definition, false, false, theStack, code, system, unit);
checkCodedElement(theErrors, thePath, element, theProfile, definition, false, false, theStack, code, system, null, unit);
}
if (code != null && "http://unitsofmeasure.org".equals(system)) {
@ -4188,7 +4194,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
} else if (element.getType().equals("CapabilityStatement")) {
validateCapabilityStatement(errors, element, stack);
} else if (element.getType().equals("CodeSystem")) {
new CodeSystemValidator(context, timeTracker, xverManager).validateCodeSystem(errors, element, stack, new ValidationOptions(stack.getWorkingLang()));
new CodeSystemValidator(context, timeTracker, xverManager).validateCodeSystem(errors, element, stack, baseOptions.setLanguage(stack.getWorkingLang()));
} else if (element.getType().equals("SearchParameter")) {
new SearchParameterValidator(context, timeTracker, fpe, xverManager).validateSearchParameter(errors, element, stack);
} else if (element.getType().equals("StructureDefinition")) {
@ -4405,7 +4411,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
}
if (definition.getPath().equals("StructureDefinition.snapshot")) {
// work around a known issue in the spec, that idsa are duplicated in snapshot and differential
// work around a known issue in the spec, that ids are duplicated in snapshot and differential
stack.resetIds();
}
@ -4993,6 +4999,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
} else {
if (nameMatches(ei.getName(), tail(ed.getPath())))
try {
// System.out.println("match slices for "+stack.getLiteralPath()+": "+slicer.getId()+" = "+slicingSummary(slicer.getSlicing()));
match = sliceMatches(hostContext, ei.getElement(), ei.getPath(), slicer, ed, profile, errors, sliceInfo, stack, profile);
if (match) {
ei.slice = slicer;
@ -5026,6 +5033,22 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
return unsupportedSlicing;
}
private String slicingSummary(ElementDefinitionSlicingComponent slicing) {
StringBuilder b = new StringBuilder();
b.append('[');
boolean first = true;
for (ElementDefinitionSlicingDiscriminatorComponent t : slicing.getDiscriminator()) {
if (first) first = false; else b.append(",");
b.append(t.getType().toCode());
b.append(":");
b.append(t.getPath());
}
b.append(']');
b.append(slicing.getOrdered() ? ";ordered" : "");
b.append(slicing.getRules().toString());
return b.toString();
}
private ElementDefinition getElementByTail(StructureDefinition p, String tail) throws DefinitionException {
if (tail == null)
return p.getSnapshot().getElement().get(0);
@ -5451,23 +5474,23 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
// no delay on this one?
public ValidationResult checkCodeOnServer(NodeStack stack, String code, String system, String display, boolean checkDisplay) {
return context.validateCode(new ValidationOptions(stack.getWorkingLang()), system, code, checkDisplay ? display : null);
public ValidationResult checkCodeOnServer(NodeStack stack, String code, String system, String version, String display, boolean checkDisplay) {
return context.validateCode(baseOptions.setLanguage(stack.getWorkingLang()), system, version, code, checkDisplay ? display : null);
}
public ValidationResult checkCodeOnServer(NodeStack stack, ValueSet valueset, Coding c, boolean checkMembership) {
if (checkMembership) {
return context.validateCode(new ValidationOptions(stack.getWorkingLang()).checkValueSetOnly(), c, valueset);
return context.validateCode(baseOptions.setLanguage(stack.getWorkingLang()).checkValueSetOnly(), c, valueset);
} else {
return context.validateCode(new ValidationOptions(stack.getWorkingLang()).noCheckValueSetMembership(), c, valueset);
return context.validateCode(baseOptions.setLanguage(stack.getWorkingLang()).noCheckValueSetMembership(), c, valueset);
}
}
public ValidationResult checkCodeOnServer(NodeStack stack, ValueSet valueset, CodeableConcept cc, boolean vsOnly) {
if (vsOnly) {
return context.validateCode(new ValidationOptions(stack.getWorkingLang()).checkValueSetOnly(), cc, valueset);
return context.validateCode(baseOptions.setLanguage(stack.getWorkingLang()).checkValueSetOnly(), cc, valueset);
} else {
return context.validateCode(new ValidationOptions(stack.getWorkingLang()), cc, valueset);
return context.validateCode(baseOptions.setLanguage(stack.getWorkingLang()), cc, valueset);
}
}
@ -5534,4 +5557,12 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
this.wantCheckSnapshotUnchanged = wantCheckSnapshotUnchanged;
}
public ValidationOptions getBaseOptions() {
return baseOptions;
}
public void setBaseOptions(ValidationOptions baseOptions) {
this.baseOptions = baseOptions;
}
}

View File

@ -77,7 +77,7 @@ public class CodeSystemValidator extends BaseValidator {
private void validateSupplementConcept(List<ValidationMessage> errors, Element concept, NodeStack stack, String supp, ValidationOptions options) {
String code = concept.getChildValue("code");
if (!Utilities.noString(code)) {
org.hl7.fhir.r5.context.IWorkerContext.ValidationResult res = context.validateCode(options, supp, code, null);
org.hl7.fhir.r5.context.IWorkerContext.ValidationResult res = context.validateCode(options, systemFromCanonical(supp), versionFromCanonical(supp), code, null);
rule(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), res.isOk(), I18nConstants.CODESYSTEM_CS_SUPP_INVALID_CODE, supp, code);
}

View File

@ -173,6 +173,7 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe
val.setWantCheckSnapshotUnchanged(true);
val.getContext().setClientRetryCount(4);
val.setDebug(false);
if (content.has("fetcher") && "standalone".equals(JSONUtil.str(content, "fetcher"))) {
val.setFetcher(vCurr);
vCurr.setFetcher(new StandAloneValidatorFetcher(vCurr.getPcm(), vCurr.getContext(), vCurr));
@ -188,6 +189,11 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe
val.setValidationLanguage(content.get("language").getAsString());
else
val.setValidationLanguage(null);
if (content.has("default-version")) {
val.setBaseOptions(val.getBaseOptions().setVersionFlexible(content.get("default-version").getAsBoolean()));
} else {
val.setBaseOptions(val.getBaseOptions().setVersionFlexible(false));
}
if (content.has("packages")) {
for (JsonElement e : content.getAsJsonArray("packages")) {
String n = e.getAsString();