
001package org.hl7.fhir.r5.testfactory; 002 003import java.io.ByteArrayOutputStream; 004import java.io.IOException; 005import java.io.PrintStream; 006import java.nio.charset.StandardCharsets; 007import java.sql.SQLException; 008import java.util.ArrayList; 009import java.util.Date; 010import java.util.HashMap; 011import java.util.List; 012import java.util.Map; 013import java.util.UUID; 014import java.util.concurrent.ThreadLocalRandom; 015 016import org.hl7.fhir.exceptions.FHIRException; 017import org.hl7.fhir.r5.elementmodel.Element; 018import org.hl7.fhir.r5.elementmodel.Manager; 019import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; 020import org.hl7.fhir.r5.fhirpath.ExpressionNode; 021import org.hl7.fhir.r5.fhirpath.FHIRPathEngine; 022import org.hl7.fhir.r5.formats.IParser.OutputStyle; 023import org.hl7.fhir.r5.liquid.BaseTableWrapper; 024import org.hl7.fhir.r5.model.Base; 025import org.hl7.fhir.r5.model.CanonicalType; 026import org.hl7.fhir.r5.model.DataType; 027import org.hl7.fhir.r5.model.DateTimeType; 028import org.hl7.fhir.r5.model.DateType; 029import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent; 030import org.hl7.fhir.r5.model.Property; 031import org.hl7.fhir.r5.model.StructureDefinition; 032import org.hl7.fhir.r5.model.ValueSet; 033import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; 034import org.hl7.fhir.r5.profilemodel.PEBuilder; 035import org.hl7.fhir.r5.profilemodel.PEBuilder.PEElementPropertiesPolicy; 036import org.hl7.fhir.r5.profilemodel.PEDefinition; 037import org.hl7.fhir.r5.profilemodel.PEType; 038import org.hl7.fhir.r5.terminologies.ValueSetUtilities; 039import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome; 040import org.hl7.fhir.r5.testfactory.TestDataFactory.DataTable; 041import org.hl7.fhir.r5.testfactory.dataprovider.BaseDataTableProvider; 042import org.hl7.fhir.r5.testfactory.dataprovider.TableDataProvider; 043import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 044import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage; 045import org.hl7.fhir.utilities.Utilities; 046import org.hl7.fhir.utilities.json.JsonException; 047import org.hl7.fhir.utilities.json.model.JsonArray; 048import org.hl7.fhir.utilities.json.model.JsonElement; 049import org.hl7.fhir.utilities.json.model.JsonObject; 050 051/** 052 * 053 * see https://build.fhir.org/ig/FHIR/ig-guidance/testfactory.html for doco 054 * 055 */ 056@MarkedToMoveToAdjunctPackage 057public class ProfileBasedFactory { 058 059 private BaseDataTableProvider baseData; 060 private TableDataProvider data; 061 private JsonArray mappings; 062 private Map<String, DataTable> tables; 063 private FHIRPathEngine fpe; 064 private PrintStream log; 065 private boolean testing; 066 private boolean markProfile; 067 068 private static class LogSet { 069 public LogSet(String msg) { 070 line.append(msg); 071 } 072 private StringBuilder line = new StringBuilder(); 073 private List<String> others = new ArrayList<>(); 074 } 075 private List<LogSet> logEntries = new ArrayList<>(); 076 077 public ProfileBasedFactory(FHIRPathEngine fpe, String baseDataSource) throws JsonException, IOException, SQLException { 078 super(); 079 this.fpe = fpe; 080 baseData = new BaseDataTableProvider(baseDataSource); 081 } 082 083 public ProfileBasedFactory(FHIRPathEngine fpe, String baseDataSource, TableDataProvider data, Map<String, DataTable> tables, JsonArray mappings) throws JsonException, IOException, SQLException { 084 super(); 085 this.fpe = fpe; 086 baseData = new BaseDataTableProvider(baseDataSource); 087 this.data = data; 088 this.tables = tables; 089 this.mappings = mappings; 090 } 091 092 public byte[] generateFormat(StructureDefinition profile, FhirFormat format) throws FHIRException, IOException, SQLException { 093 PEBuilder builder = new PEBuilder(fpe.getWorker(), PEElementPropertiesPolicy.NONE, true); 094 PEDefinition definition = builder.buildPEDefinition(profile); 095 Element element = Manager.build(fpe.getWorker(), profile); 096 097 log("--------------------------------"); 098 log("Build Row "+data.cell("counter")+" for "+profile.getVersionedUrl()); 099 if (data != null) { 100 log("Row Data: "+CommaSeparatedStringBuilder.join(",", data.cells())); 101 } 102 populateByProfile(element, definition, 0, null, null); 103 for (LogSet ls : logEntries) { 104 log(ls.line.toString()); 105 for (String s : ls.others) { 106 log(" "+s); 107 } 108 } 109 log("--------------------------------"); 110 logEntries.clear(); 111 112 if (markProfile) { 113 Element meta = element.forceElement("meta"); 114 Element prof = meta.forceElement("profile"); 115 prof.setValue(profile.getVersionedUrl()); 116 } 117 118 ByteArrayOutputStream ba = new ByteArrayOutputStream(); 119 Manager.compose(fpe.getWorker(), element, ba, format, OutputStyle.PRETTY, null); 120 return ba.toByteArray(); 121 } 122 123 124 public Element generate(StructureDefinition profile) throws FHIRException, IOException, SQLException { 125 PEBuilder builder = new PEBuilder(fpe.getWorker(), PEElementPropertiesPolicy.NONE, true); 126 PEDefinition definition = builder.buildPEDefinition(profile); 127 Element element = Manager.build(fpe.getWorker(), profile); 128 129 log("--------------------------------"); 130 log("Build Row "+data.cell("counter")+" for "+profile.getVersionedUrl()); 131 if (data != null) { 132 log("Row Data: "+CommaSeparatedStringBuilder.join(",", data.cells())); 133 } 134 populateByProfile(element, definition, 0, null, null); 135 for (LogSet ls : logEntries) { 136 log(ls.line.toString()); 137 for (String s : ls.others) { 138 log(" "+s); 139 } 140 } 141 log("--------------------------------"); 142 logEntries.clear(); 143 144 return element; 145 } 146 147 protected void populateByProfile(Element element, PEDefinition definition, int level, String path, Map<String, String> values) throws SQLException, IOException { 148 if (definition.types().size() == 1) { 149 for (PEDefinition pe : definition.directChildren(true)) { 150 if (pe.max() > 0 && (!isIgnoredElement(pe.definition().getBase().getPath()) || pe.hasFixedValue())) { 151 populateElement(element, pe, level, path, values); 152 } 153 } 154 } 155 } 156 157 private boolean isIgnoredElement(String path) { 158 return Utilities.existsInList(path, "Identifier.assigner", "Resource.meta", "DomainResource.text", "Resource.implicitRules"); 159 } 160 161 public void populateElement(Element element, PEDefinition pe, int level, String path, Map<String, String> values) throws SQLException, IOException { 162 LogSet ls = new LogSet(pe.path()+" : "); 163 logEntries.add(ls); 164 if (!pe.isExtension() && "Extension".equals(pe.typeSummary())) { 165 ls.line.append("ignore unprofiled extension"); 166 } else if (pe.isSlicer()) { 167 ls.line.append("ignore (slicer)"); 168 } else if (isNonAbstractType(pe) || pe.hasFixedValue() || pe.definition().getBase().getPath().equals("Resource.id")) { 169 if (pe.hasFixedValue()) { 170 Element focus = element.addElement(pe.schemaName()); 171 Base fv = pe.definition().hasPattern() ? pe.definition().getPattern() : pe.definition().getFixed(); 172 if (fv.isPrimitive()) { 173 ls.line.append("fixed value = "+fv.primitiveValue()); 174 focus.setValue(fv.primitiveValue()); 175 } else { 176 ls.line.append("fixed value = "+new org.hl7.fhir.r5.formats.JsonParser().setOutputStyle(OutputStyle.NORMAL).composeString((DataType) fv, "data")); 177 populateElementFromDataType(focus, fv, null); 178 } 179 } else { 180 if (pe.isSlice() && values != null) { 181 values = null; 182 ls.others.add("slice, so ignore values from parent"); 183 } 184 makeChildElement(element, pe, level, path, values, ls); 185 } 186 } else { 187 ls.line.append("ignore (type = "+pe.typeSummary()+")"); 188 } 189 } 190 191 private boolean isNonAbstractType(PEDefinition pe) { 192 for (PEType t : pe.types()) { 193 if (!pe.getBuilder().getContextUtilities().isAbstractType(t.getType()) || Utilities.existsInList(t.getType(), "BackboneElement", "BackboneType")) { 194 return true; 195 } 196 } 197 return false; 198 } 199 200 public void makeChildElement(Element element, PEDefinition pe, int level, String path, Map<String, String> values, LogSet ls) throws SQLException, IOException { 201 Element b = null; 202 if (pe.schemaName().endsWith("[x]")) { 203 if (pe.types().size() == 1) { 204 b = element.makeElement(pe.schemaName().replace("[x]", Utilities.capitalize(pe.types().get(0).getType()))); 205 } else { 206 // we could pick any, but which we pick might be dictated by the value provider 207 String t = getValueType(ls, path, pe.path(), pe.definition().getId(), pe.definition().getPath()); 208 if (t == null) { 209 // all right we just pick one 210 t = pe.types().get(testing ? 0 : ThreadLocalRandom.current().nextInt(0, pe.types().size())).getType(); 211 } 212 if (t == null) { 213 ls.line.append("ignored because polymorphic and no type"); 214 } else { 215 b = element.makeElement(pe.schemaName().replace("[x]", Utilities.capitalize(t))); 216 } 217 } 218 } else { 219 b = element.makeElement(pe.schemaName()); 220 } 221 if (b != null) { 222 if (b.isPrimitive()) { 223 String val = null; 224 if (values != null) { 225 val = values.get(b.getName()); 226 if (pe.path().endsWith(".display")) { 227 if (!valuesMatch(values.get("system"), b.getNamedChildValue("system")) || !valuesMatch(values.get("code"), b.getNamedChildValue("code"))) { 228 val = ""; 229 } 230 } 231 } 232 if (values == null || val != null || pe.min() > 0) { 233 if (val == null && data != null) { 234 val = getPrimitiveValue(ls, b.fhirType(), path, pe.path(), pe.definition().getId(), pe.definition().getPath()); 235 } 236 if (val == null && pe.valueSet() != null) { 237 ValueSetExpansionContainsComponent cc = doExpansion(ls, pe.valueSet()); 238 if (cc != null) { 239 val = cc.getCode(); 240 } 241 } 242 if (val == null) { 243 val = getBasePrimitiveValue(ls, pe, path, b); 244 } 245 if (val != null) { 246 if (Utilities.noString(val)) { 247 ls.line.append(" value suppressed"); 248 element.removeChild(b); 249 } else { 250 ls.line.append("from value "+val); 251 b.setValue(val); 252 } 253 } else { 254 ls.line.append(" fake value"); 255 switch (b.fhirType()) { 256 case "id": 257 b.setValue(makeUUID()); 258 break; 259 case "string": 260 b.setValue("Some String value"); 261 break; 262 case "base64Binary" : 263 b.setValue(java.util.Base64.getMimeEncoder().encodeToString("Some Binary Value".getBytes(StandardCharsets.UTF_8))); 264 break; 265 case "boolean" : 266 b.setValue(testing ? "true" : ThreadLocalRandom.current().nextInt(0, 2) == 1 ? "true" : "false"); 267 break; 268 case "date" : 269 b.setValue(new DateType(new Date()).asStringValue()); 270 break; 271 case "dateTime": 272 b.setValue(new DateTimeType(new Date()).asStringValue()); 273 break; 274 case "positiveInt" : 275 b.setValue(Integer.toString(testing ? 1 : ThreadLocalRandom.current().nextInt(1, 1000))); 276 break; 277 case "usignedInt" : 278 b.setValue(Integer.toString(testing ? 2 : ThreadLocalRandom.current().nextInt(0, 1000))); 279 break; 280 case "url" : 281 b.setValue("http://some.url/path"); 282 break; 283 284 default: 285 ls.others.add("Unhandled type: "+b.fhirType()); 286 } 287 } 288 } else { 289 ls.line.append(" omitted - not in values"); 290 } 291 } else { 292 boolean build = true; 293 if (values != null) { 294 values = filterValues(values, b.getName()); 295 if (values == null && pe.min() == 0) { 296 build = false; 297 } 298 } 299 if (build) { 300 if (pe.isExtension()) { 301 if (Utilities.isAbsoluteUrl(pe.getExtensionUrl()) || path == null) { 302 path = pe.getExtensionUrl(); 303 } else { 304 path = path+"."+pe.getExtensionUrl(); 305 } 306 } 307 if (values == null && data != null) { 308 values = getComplexValue(ls, b.fhirType(), path, pe.path(), pe.definition().getId(), pe.definition().getPath()); 309 } 310 if (values == null && pe.valueSet() != null) { 311 ValueSetExpansionContainsComponent cc = doExpansion(ls, pe.valueSet()); 312 if (cc != null) { 313 values = makeValuesForCodedValue(ls, b.fhirType(), cc); 314 } 315 } 316 if (values == null) { 317 if ("Reference".equals(b.fhirType()) && values == null) { 318 List<CanonicalType> targets = new ArrayList<>(); 319 for (TypeRefComponent tr : pe.definition().getType()) { 320 if (tr.getWorkingCode().equals("Reference")) { 321 targets.addAll(tr.getTargetProfile()); 322 } 323 } 324 List<String> choices = new ArrayList<>(); 325 for (CanonicalType ct : targets) { 326 StructureDefinition sd = fpe.getWorker().fetchResource(StructureDefinition.class, ct.primitiveValue()); 327 if (!Utilities.existsInList(sd.getType(), "Resource", "DomainResource")) { 328 choices.add(sd.getType()); 329 } 330 } 331 if (choices.isEmpty()) { 332 choices.addAll(fpe.getWorker().getResourceNames()); 333 } 334 String resType = choices.get(testing ? 0 : ThreadLocalRandom.current().nextInt(0, choices.size())); 335 values = new HashMap<String, String>(); 336 values.put("reference", resType+"/"+makeUUID()); 337 ls.others.add("construct reference to "+resType+" from choices: "+CommaSeparatedStringBuilder.join("|", choices)); 338 } else { 339 values = getBaseComplexValue(ls, pe, path, b); 340 } 341 } 342 if (values == null) { 343 ls.line.append(" populate children"); 344 } else if (values.isEmpty()) { 345 ls.line.append(" don't populate - no children"); 346 } else { 347 ls.line.append(" populate children from "+values.toString()); 348 } 349 if (values == null || !values.isEmpty()) { 350 populateByProfile(b, pe, level+1, path, values); 351 if (!b.hasChildren() && !b.hasValue()) { 352 element.removeChild(b); 353 } 354 } else { 355 element.removeChild(b); 356 } 357 } else { 358 ls.line.append(" omitted - values have no value"); 359 element.removeChild(b); 360 } 361 } 362 } 363 } 364 365 public String makeUUID() { 366 return testing ? "6e4d3a43-6642-4a0b-9b67-48c29af581a9" : UUID.randomUUID().toString().toLowerCase(); 367 } 368 369 370 private boolean valuesMatch(String v1, String v2) { 371 if (v1 == null) { 372 return v2 == null; 373 } else { 374 return v1.equals(v2); 375 } 376 } 377 378 private boolean hasFixedChildren(PEDefinition definition) { 379 if (definition.types().size() != 1) { 380 return false; 381 } 382 for (PEDefinition pe : definition.directChildren(true)) { 383 if (pe.hasFixedValue()) { 384 return true; 385 } 386 } 387 return false; 388 } 389 390 391 private Map<String, String> makeValuesForCodedValue(LogSet ls, String fhirType, ValueSetExpansionContainsComponent cc) { 392 Map<String, String> res = new HashMap<>(); 393 switch (fhirType) { 394 case "Coding": 395 res.put("system", cc.getSystem()); 396 if (cc.hasVersion()) { 397 res.put("version", cc.getVersion()); 398 } 399 res.put("code", cc.getCode()); 400 if (cc.hasDisplay()) { 401 res.put("display", cc.getDisplay()); 402 } 403 break; 404 case "CodeableConcept": 405 res.put("coding.system", cc.getSystem()); 406 if (cc.hasVersion()) { 407 res.put("coding.version", cc.getVersion()); 408 } 409 res.put("coding.code", cc.getCode()); 410 if (cc.hasDisplay()) { 411 res.put("coding.display", cc.getDisplay()); 412 } 413 break; 414 case "CodedReference": 415 res.put("concept.coding.system", cc.getSystem()); 416 if (cc.hasVersion()) { 417 res.put("concept.coding.version", cc.getVersion()); 418 } 419 res.put("concept.coding.code", cc.getCode()); 420 if (cc.hasDisplay()) { 421 res.put("concept.coding.display", cc.getDisplay()); 422 } 423 break; 424 case "Quantity": 425 res.put("system", cc.getSystem()); 426 res.put("code", cc.getCode()); 427 if (cc.hasDisplay()) { 428 res.put("unit", cc.getDisplay()); 429 } 430 break; 431 default: 432 ls.others.add("Unknown type handling coded value: "+fhirType); 433 return null; 434 } 435 return res; 436 } 437 438 private ValueSetExpansionContainsComponent doExpansion(LogSet ls, ValueSet vs) { 439 ValueSetExpansionOutcome vse = fpe.getWorker().expandVS(vs, true, false, 100); 440 if (vse.isOk()) { 441 ls.others.add("ValueSet "+vs.getVersionedUrl()+" "+ValueSetUtilities.countExpansion(vse.getValueset().getExpansion().getContains())+" concepts"); 442 if (testing) { 443 for (ValueSetExpansionContainsComponent cc : vse.getValueset().getExpansion().getContains()) { 444 ls.others.add(cc.getSystem()+"#"+cc.getCode()+" : \""+cc.getDisplay()+"\" ("+cc.hasContains()+")"); 445 } 446 } 447 return pickRandomConcept(vse.getValueset().getExpansion().getContains()); 448 } else { 449 ls.others.add("ValueSet "+vs.getVersionedUrl()+": error = "+vse.getError()); 450 return null; 451 } 452 } 453 454 public Map<String, String> getBaseComplexValue(LogSet ls, PEDefinition pe, String path, Element b) throws SQLException { 455 Map<String, String> result = baseData.getComplexValue(path != null ? path : pe.definition().getId(), b.fhirType()); 456 if (result == null) { 457 ls.others.add("No base data for "+path+":"+b.fhirType()); 458 } else { 459 ls.others.add("Base data for "+path+":"+b.fhirType()+" = "+result.toString()); 460 } 461 return result; 462 } 463 464 public String getBasePrimitiveValue(LogSet ls, PEDefinition pe, String path, Element b) throws SQLException { 465 String result = baseData.getPrimitiveValue(path != null ? path : pe.definition().getId(), b.fhirType()); 466 if (result == null) { 467 ls.others.add("No base data for "+path+":"+b.fhirType()); 468 } else { 469 ls.others.add("Base data for "+path+":"+b.fhirType()+" = "+result); 470 } 471 return result; 472 } 473 474 475 private Map<String, String> filterValues(Map<String, String> values, String name) { 476 Map<String, String> result = new HashMap<>(); 477 for (String s : values.keySet()) { 478 if (s.startsWith(name+".")) { 479 result.put(s.substring(name.length()+1), values.get(s)); 480 } 481 } 482 if (result.isEmpty()) { 483 return null; 484 } else { 485 return result; 486 } 487 } 488 489 private ValueSetExpansionContainsComponent pickRandomConcept(List<ValueSetExpansionContainsComponent> list) { 490 ValueSetExpansionContainsComponent res = null; 491 int i = 0; 492 while (res == null && list.size() > 0) { 493 int r = testing ? i : ThreadLocalRandom.current().nextInt(0, list.size()); 494 if (list.get(r).getAbstract()) { 495 if (list.get(r).hasContains()) { 496 res = pickRandomConcept(list.get(0).getContains()); 497 } 498 } else { 499 res = list.get(r); 500 } 501 i++; 502 } 503 return res; 504 } 505 506 private void populateElementFromDataType(Element element, Base source, PEDefinition defn) { 507 for (Property prop : source.children()) { 508 for (Base b : prop.getValues()) { 509 Element child = element.makeElement(prop.getName()); 510 if (b.isPrimitive()) { 511 child.setValue(b.primitiveValue()); 512 } else { 513 populateElementFromDataType(child, b, null); 514 } 515 } 516 } 517 } 518 519 520 private String getValueType(LogSet ls, String... ids) { 521 JsonObject entry = findMatchingEntry(ls, ids); 522 if (entry != null) { 523 JsonElement fhirType = entry.get("fhirType"); 524 if (fhirType == null || !fhirType.isJsonPrimitive() || Utilities.noString(fhirType.asString())) { 525 return ""; 526 } else { 527 String ft = fhirType.asString(); 528 StructureDefinition sd = fpe.getWorker().fetchTypeDefinition(ft); 529 if (sd != null) { 530 return ft; 531 } else { 532 return evaluateExpression(ls.others, fhirType, null); 533 } 534 } 535 } 536 return null; 537 } 538 539 private String getPrimitiveValue(LogSet ls, String fhirType, String... ids) { 540 JsonObject entry = findMatchingEntry(ls, ids); 541 if (entry != null) { 542 JsonElement expression = entry.get("expression"); 543 if (expression == null || !expression.isJsonPrimitive() || Utilities.noString(expression.asString())) { 544 ls.others.add("Found an entry for "+entry.asString("path")+" but it had no expression"); 545 return ""; 546 } else { 547 return evaluateExpression(ls.others, expression, null); 548 } 549 } 550 return null; 551 } 552 553 public String evaluateExpression(List<String> log, JsonElement expression, String name) { 554 ExpressionNode expr = (ExpressionNode) expression.getUserData("compiled"); 555 if (expr == null) { 556 expr = fpe.parse(expression.asString()); 557 expression.setUserData("compiled", expr); 558 } 559 BaseTableWrapper csv = BaseTableWrapper.forRow(data.columns(), data.cells()).setTables(tables); 560 561 String val = null; 562 try { 563 val = fpe.evaluateToString(null, null, null, csv, expr); 564 log.add(name+" ==> '"+val+"' (from "+expr.toString()+")"); 565 } catch (Exception e) { 566 log.add(name+" ==> null because "+e.getMessage()+" (from "+expr.toString()+")"); 567 } 568 return val; 569 } 570 571 private JsonObject findMatchingEntry(LogSet ls, String[] ids) { 572 for (JsonObject entry : mappings.asJsonObjects()) { 573 if (Utilities.existsInList(entry.asString("path"), ids)) { 574 boolean use = true; 575 if (entry.has("if")) { 576 use = Utilities.existsInList(evaluateExpression(ls.others, entry.get("if"), "if"), "1", "true"); 577 } 578 if (use) { 579 ls.others.add("mapping entry for "+entry.asString("path")+" from ids "+CommaSeparatedStringBuilder.join(";", ids)); 580 return entry; 581 } 582 } 583 } 584 ls.others.add("mapping entry not found for ids "+CommaSeparatedStringBuilder.join(";", ids)); 585 return null; 586 } 587 588 private Map<String, String> getComplexValue(LogSet ls, String fhirType, String... ids) { 589 Map<String, String> result = new HashMap<>(); 590 JsonObject entry = findMatchingEntry(ls, ids); 591 if (entry != null) { 592 JsonArray a = entry.forceArray("parts"); 593 if (a.size() == 0) { 594 return result; 595 } else { 596 for (JsonObject src : a.asJsonObjects()) { 597 if (!src.has("name")) { 598 throw new FHIRException("Found an entry for "+entry.asString("path")+" but it had no proeprty name"); 599 } 600 result.put(src.asString("name"), evaluateExpression(ls.others, src.get("expression"), src.asString("name"))); 601 } 602 } 603 } 604 return result.isEmpty() ? null : result; 605 } 606 607 608 private void log(String msg) throws IOException { 609 if (log != null) { 610 log.append(msg+"\r\n"); 611 } 612 } 613 614 public PrintStream getLog() { 615 return log; 616 } 617 618 public void setLog(PrintStream log) { 619 this.log = log; 620 } 621 622 public boolean isTesting() { 623 return testing; 624 } 625 626 public void setTesting(boolean testing) { 627 this.testing = testing; 628 baseData.setTesting(testing); 629 } 630 631 public boolean isMarkProfile() { 632 return markProfile; 633 } 634 635 public void setMarkProfile(boolean markProfile) { 636 this.markProfile = markProfile; 637 } 638 639 640}