1 10 package org.mmbase.module; 11 12 import org.mmbase.util.*; 13 import org.mmbase.module.core.*; 14 15 import java.util.*; 16 import java.io.*; 17 18 import org.w3c.dom.*; 19 import org.xml.sax.*; 20 import javax.xml.parsers.*; 21 22 import org.mmbase.util.logging.*; 23 24 34 35 public class TransactionHandler extends Module implements TransactionHandlerInterface { 36 37 private static Logger log = Logging.getLoggerInstance(TransactionHandler.class.getName()); 38 private static sessionsInterface sessions = null; 39 private static MMBase mmbase = null; 40 private static Upload upload = null; 41 private static String version = "2.3.8"; 42 private static boolean needs_key = true; private static String securityMode = "none"; 45 46 private static Hashtable transactionsOfUser = new Hashtable(); 48 private static TransactionManagerInterface transactionManager; 50 private static TemporaryNodeManagerInterface tmpObjectManager; 52 53 public TransactionHandler() {} 54 55 58 public void init() { 59 log.info("Module TransactionHandler (" + version + ") started"); 60 mmbase = (MMBase)getModule("MMBASEROOT"); 61 upload = (Upload)getModule("upload"); 62 sessions = (sessionsInterface)getModule("SESSION"); 63 tmpObjectManager = new TemporaryNodeManager(mmbase); 64 transactionManager = new TransactionManager(mmbase, tmpObjectManager); 65 needs_key = (getInitParameter("keycode") != null); 67 securityMode = getInitParameter("security"); 68 if (securityMode == null) 69 securityMode = "none"; 70 log.debug(">> needs key: " + needs_key + " mode: " + securityMode); 71 } 73 74 77 public void onload() { 78 log.debug("TransactionHandler onload"); 79 } 80 81 84 private String getXMLHeader() { 85 return "<?xml version='1.0'?>\n" 86 + "<!DOCTYPE transactions PUBLIC \"-//MMBase//DTD transactions config 1.0//EN\" " 87 + "\"http://www.mmbase.org/dtd/transactions_1_0.dtd\">\n"; 88 } 89 90 96 public void handleTransaction(String template, sessionInfo session, scanpage sp) { 97 98 template = getXMLHeader() + template; 99 log.service("TransactionHandler processing TCP"); 100 log.debug("Received template is:"); 101 log.debug(template); 102 103 InputSource is = new InputSource(); 104 is.setCharacterStream(new StringReader(template)); 105 String user = session.getCookie(); 107 UserTransactionInfo uti = userInfo(user); 109 uti.trace = new ParseTrace(); 111 112 try { 113 parse(null, is, uti); 114 } catch (TransactionHandlerException t) { 115 sessions.setValue(session, "TRANSACTIONTRACE", uti.trace.getTrace()); 117 sessions.setValue(session, "TRANSACTIONOPERATOR", t.transactionOperator); 118 sessions.setValue(session, "TRANSACTIONID", t.transactionId); 119 sessions.setValue(session, "OBJECTOPERATOR", t.objectOperator); 120 sessions.setValue(session, "OBJECTID", t.objectId); 121 sessions.setValue(session, "FIELDOPERATOR", t.fieldOperator); 122 sessions.setValue(session, "FIELDNAME", t.fieldId); 123 sessions.setValue(session, "TRANSACTIONERROR", t.getClass() + ": " + t.getMessage()); 124 log.error("Transaction Error:"); 125 log.error("TransactionTrace " + uti.trace.getTrace()); 126 log.error("TransactionOperator " + t.transactionOperator); 127 log.error("TransactionId " + t.transactionId); 128 log.error("ObjectOperator " + t.objectOperator); 129 log.error("ObjectId " + t.objectId); 130 log.error("FieldOperator " + t.fieldOperator); 131 log.error("Fieldname " + t.fieldId); 132 log.error("TransActionError " + t.toString()); 133 log.error("ExceptionPage " + t.exceptionPage + "\n"); 134 sp.res.setStatus(302, "OK"); 136 sp.res.setHeader("Location", t.exceptionPage); 137 } 138 } 139 140 143 private void parse(String xFile, InputSource iSource, UserTransactionInfo userTransactionInfo) throws TransactionHandlerException { 144 Document document = null; 145 Element docRootElement; 146 NodeList transactionContextList = null; 147 String exceptionPage = "exception.shtml"; 148 XMLCheckErrorHandler errorHandler = new XMLCheckErrorHandler(); 149 150 DocumentBuilder db = XMLBasicReader.getDocumentBuilder(true, (ErrorHandler)errorHandler); 151 152 try { 153 if (xFile != null) { 154 log.debug("parsing file: " + xFile); 155 document = db.parse(xFile); 156 } else { 157 if (iSource != null) { 158 log.debug("parsing input: " + iSource.toString()); 159 160 document = db.parse(iSource); 161 } else { 162 log.error("No xFile and no iSource file received!"); 163 } 164 } 165 Iterator i = errorHandler.getResultList().iterator(); 166 while (i.hasNext()) { 167 log.error("" + i.next()); 168 } 169 } catch (Exception e) { 170 e.printStackTrace(); 171 TransactionHandlerException te = new TransactionHandlerException(e.getMessage()); 172 throw te; 173 } 174 175 try { 176 docRootElement = document.getDocumentElement(); 178 179 exceptionPage = docRootElement.getAttribute("exceptionPage"); 181 if (exceptionPage.equals("")) { 182 exceptionPage = "exception.shtml"; 183 } 184 185 if (needs_key) { 187 String key; 188 key = docRootElement.getAttribute("key"); 189 190 if ((key == null) || (!key.equals(getInitParameter("keycode")))) { 191 if (securityMode.equals("signal")) { 192 log.info("Transaction (TCP) key is incorrect." + " TCP key='" + key + "' Server TCP key='" + getInitParameter("keycode") + "'"); 193 } 194 if (securityMode.equals("secure")) { 195 log.error("Transaction (TCP) key is incorrect." + " TCP key='" + key + "' Server TCP key='" + getInitParameter("keycode") + "'"); 196 TransactionHandlerException te = 197 new TransactionHandlerException( 198 "Transaction (TCP) key is incorrect." + " TCP key='" + key + "' Server TCP key='" + getInitParameter("keycode") + "'"); 199 te.exceptionPage = exceptionPage; 200 throw te; 201 } 202 } 203 } 204 205 transactionContextList = docRootElement.getChildNodes(); 207 208 } catch (Exception t) { 209 log.error("Error in reading transaction."); 210 } 211 212 try { 214 evaluateTransactions(transactionContextList, userTransactionInfo); 215 } catch (TransactionHandlerException te) { 216 te.exceptionPage = exceptionPage; 217 throw te; 218 } 219 220 log.debug("exiting parse method"); 221 } 222 223 private void evaluateTransactions(NodeList transactionContextList, UserTransactionInfo userTransactionInfo) throws TransactionHandlerException { 224 Node currentTransactionArgumentNode; 225 String currentTransactionContext; 226 boolean anonymousTransaction = true; 227 Node transactionContext; 228 TransactionInfo transactionInfo = null; 229 230 for (int i = 0; i < transactionContextList.getLength(); i++) { 231 currentTransactionArgumentNode = null; 233 currentTransactionContext = null; 234 String id = null, commit = null, time = null; 235 236 transactionContext = transactionContextList.item(i); 237 String tName = transactionContext.getNodeName(); 238 if (tName.equals("#text")) 239 continue; 240 241 NamedNodeMap nm = transactionContext.getAttributes(); 243 if (nm != null) { 244 currentTransactionArgumentNode = nm.getNamedItem("id"); 246 if (currentTransactionArgumentNode != null) { 247 id = currentTransactionArgumentNode.getNodeValue(); 248 } 249 currentTransactionArgumentNode = nm.getNamedItem("commit"); 251 if (currentTransactionArgumentNode != null) { 252 commit = currentTransactionArgumentNode.getNodeValue(); 253 } 254 currentTransactionArgumentNode = nm.getNamedItem("timeOut"); 256 if (currentTransactionArgumentNode != null) { 257 time = currentTransactionArgumentNode.getNodeValue(); 258 } 259 } 260 262 if (id == null) { 264 anonymousTransaction = true; 265 id = uniqueId(); 266 } else { 267 anonymousTransaction = false; 268 } 269 if (commit == null) 270 commit = "true"; 271 if (time == null) 272 time = "60"; 273 274 log.debug("-> " + tName + " id(" + id + ") commit(" + commit + ") time(" + time + ")"); 275 userTransactionInfo.trace.addTrace(tName + " id(" + id + ") commit(" + commit + ") time(" + time + ")", 1, true); 276 277 try { 278 if (tName.equals("create")) { 280 if (userTransactionInfo.knownTransactionContexts.get(id) != null) { 282 throw new TransactionHandlerException(tName + " transaction already exists id = " + id); 283 } 284 currentTransactionContext = transactionManager.create(userTransactionInfo.user, id); 286 transactionInfo = new TransactionInfo(currentTransactionContext, time, id, userTransactionInfo); 287 if (!anonymousTransaction) { 289 userTransactionInfo.knownTransactionContexts.put(id, transactionInfo); 290 } 291 } else { 292 293 if (tName.equals("open")) { 294 if (userTransactionInfo.knownTransactionContexts.get(id) == null) { 297 throw new TransactionHandlerException(tName + " transaction doesn't exists id = " + id); 298 } 299 transactionInfo = (TransactionInfo)userTransactionInfo.knownTransactionContexts.get(id); 301 currentTransactionContext = transactionInfo.transactionContext; 302 } else { 303 304 if (tName.equals("commit")) { 305 if (anonymousTransaction == true) { 306 throw new TransactionHandlerException("commit tag needs id attribure"); 307 } 308 if (userTransactionInfo.knownTransactionContexts.get(id) == null) { 309 throw new TransactionHandlerException("Transaction '" + id + "' is probably already committed, check attribute commit=false"); 310 } 311 transactionInfo = (TransactionInfo)userTransactionInfo.knownTransactionContexts.get(id); 313 currentTransactionContext = transactionInfo.transactionContext; 314 transactionManager.commit(userTransactionInfo.user, currentTransactionContext); 315 transactionInfo.stop(); 317 continue; 319 } else { 320 321 if (tName.equals("delete")) { 322 transactionManager.cancel(userTransactionInfo.user, id); 324 TransactionInfo ti = (TransactionInfo)userTransactionInfo.knownTransactionContexts.get(id); 326 ti.stop(); 328 continue; 330 } else { 331 throw new TransactionHandlerException("transaction operator " + tName + " doesn't exist"); 332 } 333 } 334 } 335 } 336 337 NodeList objectContextList = transactionContext.getChildNodes(); 340 evaluateObjects(objectContextList, userTransactionInfo, currentTransactionContext, transactionInfo); 342 343 if (tName.equals("create") || tName.equals("open")) { 347 if (commit.equals("true")) { 348 transactionManager.commit(userTransactionInfo.user, currentTransactionContext); 349 transactionInfo.stop(); 350 if (!anonymousTransaction) { 351 userTransactionInfo.knownTransactionContexts.remove(id); 352 } 353 } 354 } 355 364 log.debug("<- " + tName + " id(" + id + ") commit(" + commit + ") time(" + time + ")"); 365 } catch (Exception e) { 367 e.printStackTrace(); 368 TransactionHandlerException t = null; 369 if (e instanceof TransactionHandlerException) { 370 t = (TransactionHandlerException)e; 371 } else { 372 t = new TransactionHandlerException("" + e); 373 } 374 t.transactionOperator = tName; 375 t.transactionId = id; 376 throw t; 377 } 378 } 379 } 380 381 384 private void evaluateObjects( 385 NodeList objectContextList, 386 UserTransactionInfo userTransactionInfo, 387 String currentTransactionContext, 388 TransactionInfo transactionInfo) 389 throws TransactionHandlerException { 390 Node currentObjectArgumentNode = null; 391 Node objectContext; 392 NodeList fieldContextList; 393 String currentObjectContext; 394 boolean anonymousObject = true; 395 396 for (int j = 0; j < objectContextList.getLength(); j++) { 397 String id = null, type = null, oMmbaseId = null; 398 String relationSource = null, relationDestination = null; 399 String deleteRelations = "false"; 400 currentObjectContext = null; 401 402 objectContext = objectContextList.item(j); 404 String oName = objectContext.getNodeName(); 405 406 if (oName.equals("#text")) 407 continue; 408 409 NamedNodeMap nm2 = objectContext.getAttributes(); 411 if (nm2 != null) { 412 currentObjectArgumentNode = nm2.getNamedItem("id"); 413 if (currentObjectArgumentNode != null) 414 id = currentObjectArgumentNode.getNodeValue(); 415 currentObjectArgumentNode = nm2.getNamedItem("type"); 417 if (currentObjectArgumentNode != null) 418 type = currentObjectArgumentNode.getNodeValue(); 419 currentObjectArgumentNode = nm2.getNamedItem("mmbaseId"); 421 if (currentObjectArgumentNode != null) 422 oMmbaseId = currentObjectArgumentNode.getNodeValue(); 423 currentObjectArgumentNode = nm2.getNamedItem("source"); 425 if (currentObjectArgumentNode != null) 426 relationSource = currentObjectArgumentNode.getNodeValue(); 427 currentObjectArgumentNode = nm2.getNamedItem("destination"); 429 if (currentObjectArgumentNode != null) 430 relationDestination = currentObjectArgumentNode.getNodeValue(); 431 currentObjectArgumentNode = nm2.getNamedItem("deleteRelations"); 433 if (currentObjectArgumentNode != null) 434 deleteRelations = currentObjectArgumentNode.getNodeValue(); 435 } 436 if (id == null) { 437 id = uniqueId(); 438 anonymousObject = true; 439 } else { 440 anonymousObject = false; 441 } 442 443 if (oName.equals("createRelation")) { 444 log.debug(oName + " id(" + id + ") source(" + relationSource + ") destination(" + relationDestination + ")"); 445 userTransactionInfo.trace.addTrace(oName + " id(" + id + ") source(" + relationSource + ") destination(" + relationDestination + ")", 2, true); 446 } else { 447 log.debug("-> " + oName + " id(" + id + ") type(" + type + ") oMmbaseId(" + oMmbaseId + ")"); 448 userTransactionInfo.trace.addTrace(oName + " id(" + id + ") type(" + type + ") oMmbaseId(" + oMmbaseId + ")", 2, true); 449 } 450 451 try { 452 if (oName.equals("createObject")) { 453 if (transactionInfo.knownObjectContexts.get(id) != null) { 455 throw new TransactionHandlerException(oName + " Object id already exists: " + id); 456 } 457 currentObjectContext = tmpObjectManager.createTmpNode(type, userTransactionInfo.user.getName(), id); 459 if (!anonymousObject) { 460 transactionInfo.knownObjectContexts.put(id, currentObjectContext); 461 } 462 transactionManager.addNode(currentTransactionContext, userTransactionInfo.user.getName(), currentObjectContext); 464 } else { 465 if (oName.equals("createRelation")) { 466 if (transactionInfo.knownObjectContexts.get(id) != null) { 468 throw new TransactionHandlerException(oName + " Object id already exists: " + id); 469 } 470 currentObjectContext = 472 tmpObjectManager.createTmpRelationNode(type, userTransactionInfo.user.getName(), id, relationSource, relationDestination); 473 if (!anonymousObject) { 474 transactionInfo.knownObjectContexts.put(id, currentObjectContext); 475 } 476 transactionManager.addNode(currentTransactionContext, userTransactionInfo.user.getName(), currentObjectContext); 478 } else { 479 if (oName.equals("accessObject")) { 480 if (transactionInfo.knownObjectContexts.get(id) != null) { 482 throw new TransactionHandlerException(oName + " Object id already exists: " + id); 483 } 484 if (oMmbaseId == null) { 485 throw new TransactionHandlerException(oName + " no MMbase id: "); 486 } 487 currentObjectContext = tmpObjectManager.getObject(userTransactionInfo.user.getName(), id, oMmbaseId); 489 transactionManager.addNode(currentTransactionContext, userTransactionInfo.user.getName(), currentObjectContext); 491 if (!anonymousObject) 493 transactionInfo.knownObjectContexts.put(id, currentObjectContext); 494 } else { 495 if (oName.equals("openObject")) { 496 if (transactionInfo.knownObjectContexts.get(id) == null) { 497 throw new TransactionHandlerException(oName + " Object id doesn't exists: " + id); 498 } 499 currentObjectContext = (String )transactionInfo.knownObjectContexts.get(id); 500 } else { 501 if (oName.equals("deleteObject")) { 502 if (id == null) { 503 throw new TransactionHandlerException(oName + " no id specified"); 504 } 505 currentObjectContext = (String )transactionInfo.knownObjectContexts.get(id); 507 transactionManager.removeNode(currentTransactionContext, userTransactionInfo.user.getName(), currentObjectContext); 508 tmpObjectManager.deleteTmpNode(userTransactionInfo.user.getName(), currentObjectContext); 510 transactionInfo.knownObjectContexts.remove(id); 511 continue; 512 } else { 513 if (oName.equals("markObjectDelete")) { 514 if (oMmbaseId == null) { 515 throw new TransactionHandlerException(oName + " no mmbaseId specified"); 516 } 517 currentObjectContext = tmpObjectManager.getObject(userTransactionInfo.user.getName(), id, oMmbaseId); 519 transactionManager.addNode(currentTransactionContext, userTransactionInfo.user.getName(), currentObjectContext); 520 transactionManager.deleteObject(currentTransactionContext, userTransactionInfo.user.getName(), currentObjectContext); 521 522 if (transactionInfo.knownObjectContexts.containsKey(id)) { 525 transactionInfo.knownObjectContexts.remove(id); 526 } 527 528 Vector relations = mmbase.getInsRel().getRelations_main(new Integer (oMmbaseId).intValue()); 530 if (relations.size() != 0) { 531 if (deleteRelations.equals("true")) { 532 Iterator iterator = relations.iterator(); 533 while (iterator.hasNext()) { 534 MMObjectNode node = (MMObjectNode)iterator.next(); 535 oMmbaseId = "" + node.getValue("number"); 536 id = uniqueId(); 537 currentObjectContext = tmpObjectManager.getObject(userTransactionInfo.user.getName(), id, oMmbaseId); 538 transactionManager.addNode( 539 currentTransactionContext, 540 userTransactionInfo.user.getName(), 541 currentObjectContext); 542 transactionManager.deleteObject( 543 currentTransactionContext, 544 userTransactionInfo.user.getName(), 545 currentObjectContext); 546 } 547 } else { 548 throw new TransactionHandlerException( 549 "object has " + relations.size() + " relation(s) attached to it. (use deleteRelations=\"true\")"); 550 } 551 } 552 continue; 553 } else { 554 throw new TransactionHandlerException("object operator " + oName + " doesn't exist"); 555 } 556 } 557 } 558 } 559 } 560 } 561 562 fieldContextList = objectContext.getChildNodes(); 565 evaluateFields(fieldContextList, userTransactionInfo, id, currentObjectContext); 567 568 if (oName.equals("deleteObject")) {} 569 if (oName.equals("createObject")) {} 570 if (oName.equals("openObject")) {} 571 if (oName.equals("accessObject")) {} 572 573 if (oName.equals("createRelation")) { 574 log.debug("<- " + oName + " id(" + id + ") source(" + relationSource + ") destination(" + relationDestination + ")"); 575 } else { 576 log.debug("<- " + oName + " id(" + id + ") type(" + type + ") oMmbaseId(" + oMmbaseId + ")"); 577 } 578 } catch (Exception e) { 579 TransactionHandlerException t = null; 580 if (e instanceof TransactionHandlerException) { 581 t = (TransactionHandlerException)e; 582 } else { 583 t = new TransactionHandlerException("" + e); 584 } 585 t.objectOperator = oName; 586 t.objectId = id; 587 throw t; 588 } 589 } 590 } 591 592 private void evaluateFields(NodeList fieldContextList, UserTransactionInfo userTransactionInfo, String oId, String currentObjectContext) 593 throws TransactionHandlerException { 594 595 for (int k = 0; k < fieldContextList.getLength(); k++) { 596 String fieldName = null; 597 Object fieldValue = ""; 598 599 Node fieldContext = fieldContextList.item(k); 600 String nodeName = fieldContext.getNodeName(); 601 602 log.debug("nodeName = " + nodeName); 603 604 if (nodeName.equals("#text")) { 605 continue; 606 } 607 608 if (!nodeName.equals("setField")) { 609 log.error(nodeName + " is not a valid operation on an object"); 610 throw new TransactionHandlerException(nodeName + " is not a valid operation on an object"); 611 } 612 613 NamedNodeMap nm3 = fieldContext.getAttributes(); 615 if (nm3 != null) { 616 Node currentObjectArgumentNode = nm3.getNamedItem("name"); 617 if (currentObjectArgumentNode != null) { 618 fieldName = currentObjectArgumentNode.getNodeValue(); 619 } 620 if (fieldName == null) { 621 throw new TransactionHandlerException("<setField name=\"fieldname\">value</setField> is missing the NAME attribute!"); 622 } 623 624 currentObjectArgumentNode = nm3.getNamedItem("url"); 626 if (currentObjectArgumentNode != null) { 627 String url = currentObjectArgumentNode.getNodeValue(); 628 fieldValue = upload.getFile(url); 629 upload.deleteFile(url); 630 log.debug("-X Object " + oId + ": [" + fieldName + "] set to: " + url); 631 } else { 632 633 Node setFieldValue = fieldContext.getFirstChild(); 634 if (setFieldValue != null) { 635 fieldValue = setFieldValue.getNodeValue(); 636 } 637 log.debug("-X Object " + oId + ": [" + fieldName + "] set to: " + fieldValue); 638 } 639 userTransactionInfo.trace.addTrace("setField " + oId + ": [" + fieldName + "] set to: " + fieldValue, 3, true); 640 641 try { 642 tmpObjectManager.setObjectField(userTransactionInfo.user.getName(), currentObjectContext, fieldName, fieldValue); 643 } catch (Exception e) { 644 TransactionHandlerException the = new TransactionHandlerException("cannot set field '" + fieldName + "'"); 645 the.fieldId = fieldName; 646 the.fieldOperator = "SETFIELD"; 647 throw the; 648 } 649 } 650 } 651 } 652 653 private UserTransactionInfo userInfo(String user) { 654 if (!transactionsOfUser.containsKey(user)) { 655 log.debug("Create UserTransactionInfo for user " + user); 656 UserTransactionInfo uti = new UserTransactionInfo(); 658 transactionsOfUser.put(user, uti); 659 uti.user = new User(user); 660 } else { 661 log.warn("UserTransactionInfo already known for user " + user); 662 } 663 return ((UserTransactionInfo)transactionsOfUser.get(user)); 664 } 665 666 669 private synchronized String uniqueId() { 670 try { 671 Thread.sleep(1); } catch (Exception e) { 673 log.debug("What's the reason I may not sleep?"); 674 } 675 return "ID" + java.lang.System.currentTimeMillis(); 676 } 677 678 682 class User { 683 private String name; 684 685 public User(String name) { 686 this.name = name; 687 } 688 689 String getName() { 690 int length = name.length(); 691 String tempname = "TR" + name.substring(length - 8, length); 692 return tempname; 693 } 694 } 695 696 699 class UserTransactionInfo { 700 public Hashtable knownTransactionContexts = new Hashtable(); 702 public User user = null; 704 public ParseTrace trace = null; 706 } 707 708 711 class TransactionInfo implements Runnable { 712 String transactionContext = null; 714 Hashtable knownObjectContexts = new Hashtable(); 716 long timeout = 0; 718 String id = ""; 720 Thread kicker = null; 722 UserTransactionInfo uti = null; 724 boolean finished = false; 726 727 TransactionInfo(String t, String timeout, String id, UserTransactionInfo uti) { 728 this.transactionContext = t; 729 this.timeout = Long.parseLong(timeout) * 1000; 730 this.id = id; 731 this.uti = uti; 732 start(); 733 } 734 735 738 public void start() { 739 if (kicker == null) { 740 kicker = new Thread (this, "TR " + transactionContext); 741 kicker.start(); 742 } 743 } 744 745 748 public synchronized void stop() { 749 kicker = null; 750 finished = true; 751 this.notify(); 752 } 753 754 758 public void run() { 759 try { 760 synchronized (this) { 761 this.wait(timeout * 1000); 762 } 763 } catch (InterruptedException e) {} 764 uti.knownTransactionContexts.remove(id); 765 if (!finished) { 766 log.warn("Transaction with id=" + id + " is timed out after " + timeout / 1000 + " seconds."); 767 } 768 } 769 770 public String toString() { 771 return "TransactionInfo => transactionContext=" + transactionContext + " id=" + id + " timeout=" + timeout + "."; 772 } 773 } 774 775 class ParseTrace { 776 private String trace = ""; 777 778 void addTrace(String s, int indent, boolean new_line) { 779 if (new_line) 780 trace = trace + "\n<BR>"; 781 for (int i = 0; i < indent; i++) 782 trace = trace + "\t"; 783 trace = trace + s; 784 } 785 786 String getTrace() { 787 return trace; 788 } 789 } 790 } 791 | Popular Tags |