001 package railo.runtime.tag; 002 003 import java.io.IOException; 004 import java.util.Iterator; 005 import java.util.Map; 006 import java.util.Map.Entry; 007 008 import javax.servlet.jsp.JspWriter; 009 import javax.servlet.jsp.tagext.Tag; 010 011 import railo.commons.lang.StringUtil; 012 import railo.runtime.Component; 013 import railo.runtime.ComponentPro; 014 import railo.runtime.PageContext; 015 import railo.runtime.PageContextImpl; 016 import railo.runtime.component.ComponentLoader; 017 import railo.runtime.component.Member; 018 import railo.runtime.customtag.CustomTagUtil; 019 import railo.runtime.customtag.InitFile; 020 import railo.runtime.exp.ApplicationException; 021 import railo.runtime.exp.CasterException; 022 import railo.runtime.exp.ExpressionException; 023 import railo.runtime.exp.PageException; 024 import railo.runtime.exp.PageRuntimeException; 025 import railo.runtime.exp.PageServletException; 026 import railo.runtime.ext.tag.AppendixTag; 027 import railo.runtime.ext.tag.BodyTagTryCatchFinallyImpl; 028 import railo.runtime.ext.tag.DynamicAttributes; 029 import railo.runtime.op.Caster; 030 import railo.runtime.op.Decision; 031 import railo.runtime.type.Collection; 032 import railo.runtime.type.KeyImpl; 033 import railo.runtime.type.List; 034 import railo.runtime.type.Struct; 035 import railo.runtime.type.StructImpl; 036 import railo.runtime.type.scope.Caller; 037 import railo.runtime.type.scope.CallerImpl; 038 import railo.runtime.type.scope.Undefined; 039 import railo.runtime.type.scope.UndefinedImpl; 040 import railo.runtime.type.scope.Variables; 041 import railo.runtime.type.scope.VariablesImpl; 042 import railo.runtime.type.util.ArrayUtil; 043 import railo.runtime.type.util.ComponentUtil; 044 import railo.runtime.type.util.Type; 045 import railo.runtime.util.QueryStack; 046 import railo.runtime.util.QueryStackImpl; 047 import railo.transformer.library.tag.TagLibTag; 048 import railo.transformer.library.tag.TagLibTagAttr; 049 050 051 /** 052 * Creates a CFML Custom Tag 053 **/ 054 public class CFTag extends BodyTagTryCatchFinallyImpl implements DynamicAttributes,AppendixTag { 055 056 private static Collection.Key GENERATED_CONTENT=KeyImpl.intern("GENERATEDCONTENT"); 057 private static Collection.Key EXECUTION_MODE=KeyImpl.intern("EXECUTIONMODE"); 058 private static Collection.Key EXECUTE_BODY=KeyImpl.intern("EXECUTEBODY"); 059 private static Collection.Key HAS_END_TAG=KeyImpl.intern("HASENDTAG"); 060 private static Collection.Key PARENT=KeyImpl.intern("PARENT"); 061 private static Collection.Key CFCATCH=KeyImpl.intern("CFCATCH"); 062 private static Collection.Key SOURCE=KeyImpl.intern("SOURCE"); 063 064 065 private static Collection.Key ATTRIBUTES=KeyImpl.intern("ATTRIBUTES"); 066 private static Collection.Key CALLER=KeyImpl.intern("CALLER"); 067 private static Collection.Key THIS_TAG=KeyImpl.intern("THISTAG"); 068 069 070 private static final Collection.Key ON_ERROR = KeyImpl.intern("onError"); 071 private static final Collection.Key ON_FINALLY = KeyImpl.intern("onFinally"); 072 private static final Collection.Key ON_START_TAG = KeyImpl.intern("onStartTag"); 073 private static final Collection.Key ON_END_TAG = KeyImpl.intern("onEndTag"); 074 private static final Collection.Key INIT = KeyImpl.intern("init"); 075 076 private static final Collection.Key ATTRIBUTE_TYPE = KeyImpl.intern("attributetype"); 077 private static final Collection.Key RT_EXPR_VALUE = KeyImpl.intern("rtexprvalue"); 078 private static final Collection.Key PARSE_BODY = KeyImpl.intern("parsebody"); 079 private static final Collection.Key METADATA = KeyImpl.intern("metadata"); 080 private static final String MARKER = "2w12801"; 081 082 /** 083 * Field <code>attributesScope</code> 084 */ 085 // new scopes 086 protected StructImpl attributesScope; 087 private Caller callerScope; 088 private StructImpl thistagScope; 089 090 private Variables ctVariablesScope; 091 092 private boolean hasBody; 093 094 /** 095 * Field <code>filename</code> 096 */ 097 //protected String filename; 098 099 /** 100 * Field <code>source</code> 101 */ 102 protected InitFile source; 103 private String appendix; 104 //private boolean doCustomTagDeepSearch; 105 106 private ComponentPro cfc; 107 private boolean isEndTag; 108 109 110 111 /** 112 * constructor for the tag class 113 **/ 114 public CFTag() { 115 attributesScope = new StructImpl(); 116 callerScope = new CallerImpl(); 117 //thistagScope = new StructImpl(); 118 } 119 120 /** 121 * @see railo.runtime.ext.tag.DynamicAttributes#setDynamicAttribute(java.lang.String, java.lang.String, java.lang.Object) 122 */ 123 public void setDynamicAttribute(String uri, String name, Object value) { 124 TagUtil.setDynamicAttribute(attributesScope,name,value,TagUtil.UPPER_CASE); 125 } 126 127 /** 128 * @see javax.servlet.jsp.tagext.Tag#release() 129 */ 130 public void release() { 131 super.release(); 132 133 hasBody=false; 134 //filename=null; 135 136 attributesScope=new StructImpl();//.clear(); 137 callerScope = new CallerImpl(); 138 if(thistagScope!=null)thistagScope=null; 139 if(ctVariablesScope!=null)ctVariablesScope=null; 140 141 142 isEndTag=false; 143 144 //cfc=null; 145 source=null; 146 } 147 148 /** 149 * sets the appendix of the class 150 * @param appendix 151 */ 152 public void setAppendix(String appendix) { 153 this.appendix=appendix; 154 //filename = appendix+'.'+pageContext.getConfig().getCFMLExtension(); 155 } 156 157 /** 158 * @see javax.servlet.jsp.tagext.Tag#doStartTag() 159 */ 160 public int doStartTag() throws PageException { 161 PageContextImpl pci=(PageContextImpl) pageContext; 162 boolean old=pci.useSpecialMappings(true); 163 try{ 164 initFile(); 165 callerScope.initialize(pageContext); 166 if(source.isCFC())return cfcStartTag(); 167 return cfmlStartTag(); 168 } 169 finally{ 170 pci.useSpecialMappings(old); 171 } 172 173 174 175 176 } 177 178 /** 179 * @see javax.servlet.jsp.tagext.Tag#doEndTag() 180 */ 181 public int doEndTag() { 182 PageContextImpl pci=(PageContextImpl) pageContext; 183 boolean old=pci.useSpecialMappings(true); 184 try{ 185 if(source.isCFC())_doCFCFinally(); 186 return EVAL_PAGE; 187 } 188 finally{ 189 pci.useSpecialMappings(old); 190 } 191 } 192 193 /** 194 * @see javax.servlet.jsp.tagext.BodyTag#doInitBody() 195 */ 196 public void doInitBody() { 197 198 } 199 200 /** 201 * @see javax.servlet.jsp.tagext.BodyTag#doAfterBody() 202 */ 203 public int doAfterBody() throws PageException { 204 if(source.isCFC())return cfcEndTag(); 205 return cfmlEndTag(); 206 } 207 208 209 /** 210 * @see railo.runtime.ext.tag.BodyTagTryCatchFinallyImpl#doCatch(java.lang.Throwable) 211 */ 212 public void doCatch(Throwable t) throws Throwable { 213 if(source.isCFC()){ 214 String source=isEndTag?"end":"body"; 215 isEndTag=false; 216 _doCFCCatch(t,source); 217 } 218 else super.doCatch(t); 219 } 220 221 void initFile() throws PageException { 222 source=initFile(pageContext); 223 } 224 225 public InitFile initFile(PageContext pageContext) throws PageException { 226 return CustomTagUtil.loadInitFile(pageContext, appendix); 227 } 228 229 private int cfmlStartTag() throws PageException { 230 callerScope.initialize(pageContext); 231 232 // thistag 233 if(thistagScope==null)thistagScope=new StructImpl(StructImpl.TYPE_LINKED); 234 thistagScope.set(GENERATED_CONTENT,""); 235 thistagScope.set(EXECUTION_MODE,"start"); 236 thistagScope.set(EXECUTE_BODY,Boolean.TRUE); 237 thistagScope.set(HAS_END_TAG,Caster.toBoolean(hasBody)); 238 239 240 ctVariablesScope=new VariablesImpl(); 241 ctVariablesScope.setEL(ATTRIBUTES,attributesScope); 242 ctVariablesScope.setEL(CALLER,callerScope); 243 ctVariablesScope.setEL(THIS_TAG,thistagScope); 244 245 246 // include 247 doInclude(); 248 249 return Caster.toBooleanValue(thistagScope.get(EXECUTE_BODY))?EVAL_BODY_BUFFERED:SKIP_BODY; 250 } 251 252 private int cfmlEndTag() throws PageException { 253 // thistag 254 String genConBefore = bodyContent.getString(); 255 thistagScope.set(GENERATED_CONTENT,genConBefore); 256 thistagScope.set(EXECUTION_MODE,"end"); 257 thistagScope.set(EXECUTE_BODY,Boolean.FALSE); 258 writeEL(bodyContent, MARKER); 259 260 // include 261 try{ 262 doInclude(); 263 } 264 catch(Throwable t){ 265 writeOut(genConBefore); 266 throw Caster.toPageException(t); 267 } 268 269 writeOut(genConBefore); 270 271 return Caster.toBooleanValue(thistagScope.get(EXECUTE_BODY))?EVAL_BODY_BUFFERED:SKIP_BODY; 272 } 273 274 275 276 private void writeOut(String genConBefore) throws PageException { 277 String output = bodyContent.getString(); 278 bodyContent.clearBody(); 279 String genConAfter = Caster.toString(thistagScope.get(GENERATED_CONTENT)); 280 281 if(genConBefore!=genConAfter){ 282 if(output.startsWith(genConBefore+MARKER)){ 283 output=output.substring((genConBefore+MARKER).length()); 284 } 285 output=genConAfter+output; 286 } 287 else { 288 if(output.startsWith(genConBefore+MARKER)){ 289 output=output.substring((genConBefore+MARKER).length()); 290 output=genConBefore+output; 291 } 292 } 293 294 295 writeEL(bodyContent.getEnclosingWriter(),output); 296 } 297 298 private void writeEL(JspWriter writer, String str) throws PageException { 299 try { 300 writer.write(str); 301 } catch (IOException e) { 302 throw Caster.toPageException(e); 303 } 304 } 305 306 void doInclude() throws PageException { 307 Variables var=pageContext.variablesScope(); 308 pageContext.setVariablesScope(ctVariablesScope); 309 310 311 QueryStack cs=null; 312 Undefined undefined=pageContext.undefinedScope(); 313 int oldMode=undefined.setMode(Undefined.MODE_NO_LOCAL_AND_ARGUMENTS); 314 if(oldMode!=Undefined.MODE_NO_LOCAL_AND_ARGUMENTS) 315 callerScope.setScope(var,pageContext.localScope(),pageContext.argumentsScope(),true); 316 else 317 callerScope.setScope(var,null,null,false); 318 319 if(pageContext.getConfig().allowImplicidQueryCall()) { 320 cs=undefined.getQueryStack(); 321 undefined.setQueryStack(new QueryStackImpl()); 322 } 323 324 try { 325 pageContext.doInclude(source.getPageSource()); 326 } 327 catch (Throwable t) { 328 throw Caster.toPageException(t); 329 } 330 finally { 331 undefined.setMode(oldMode); 332 //varScopeData=variablesScope.getMap(); 333 pageContext.setVariablesScope(var); 334 if(pageContext.getConfig().allowImplicidQueryCall()) { 335 undefined.setQueryStack(cs); 336 } 337 } 338 339 } 340 341 342 // CFC 343 344 private int cfcStartTag() throws PageException { 345 346 callerScope.initialize(pageContext); 347 cfc = ComponentLoader.loadComponent(pageContext,null,source.getPageSource(), source.getFilename().substring(0,source.getFilename().length()-(pageContext.getConfig().getCFCExtension().length()+1)), false,true); 348 validateAttributes(cfc,attributesScope,StringUtil.ucFirst(List.last(source.getPageSource().getComponentName(),'.'))); 349 350 boolean exeBody = false; 351 try { 352 Object rtn=Boolean.TRUE; 353 if(cfc.contains(pageContext, INIT)){ 354 Tag parent=getParent(); 355 while(parent!=null && !(parent instanceof CFTag && ((CFTag)parent).isCFCBasedCustomTag())) { 356 parent=parent.getParent(); 357 } 358 Struct args=new StructImpl(StructImpl.TYPE_LINKED); 359 args.set(HAS_END_TAG, Caster.toBoolean(hasBody)); 360 if(parent instanceof CFTag) { 361 args.set(PARENT, ((CFTag)parent).getComponent()); 362 } 363 rtn=cfc.callWithNamedValues(pageContext, INIT, args); 364 } 365 366 if(cfc.contains(pageContext, ON_START_TAG)){ 367 Struct args=new StructImpl(); 368 args.set(ATTRIBUTES, attributesScope); 369 setCaller(pageContext,args); 370 371 372 373 rtn=cfc.callWithNamedValues(pageContext, ON_START_TAG, args); 374 } 375 exeBody=Caster.toBooleanValue(rtn,true); 376 } 377 catch(Throwable t){ 378 _doCFCCatch(t,"start"); 379 } 380 return exeBody?EVAL_BODY_BUFFERED:SKIP_BODY; 381 } 382 383 private static void setCaller(PageContext pageContext, Struct args) throws PageException { 384 UndefinedImpl undefined=(UndefinedImpl) pageContext.undefinedScope(); 385 args.set(CALLER, undefined.duplicate(false)); 386 //args.set(CALLER, pageContext.variablesScope()); 387 388 } 389 390 private static void validateAttributes(ComponentPro cfc,StructImpl attributesScope,String tagName) throws ApplicationException, ExpressionException { 391 392 TagLibTag tag=getAttributeRequirments(cfc,false); 393 if(tag==null) return; 394 395 if(tag.getAttributeType()==TagLibTag.ATTRIBUTE_TYPE_FIXED || tag.getAttributeType()==TagLibTag.ATTRIBUTE_TYPE_MIXED){ 396 Iterator it = tag.getAttributes().entrySet().iterator(); 397 Map.Entry entry; 398 int count=0; 399 Collection.Key key; 400 TagLibTagAttr attr; 401 Object value; 402 // check existing attributes 403 while(it.hasNext()){ 404 entry = (Entry) it.next(); 405 count++; 406 key=KeyImpl.toKey(entry.getKey(),null); 407 attr=(TagLibTagAttr) entry.getValue(); 408 value=attributesScope.get(key,null); 409 if(value==null){ 410 if(attr.getDefaultValue()!=null){ 411 value=attr.getDefaultValue(); 412 attributesScope.setEL(key, value); 413 } 414 else if(attr.isRequired()) 415 throw new ApplicationException("attribute ["+key.getString()+"] is required for tag ["+tagName+"]"); 416 } 417 if(value!=null) { 418 if(!Decision.isCastableTo(attr.getType(),value,true)) 419 throw new CasterException(createMessage(attr.getType(), value)); 420 421 } 422 } 423 424 // check if there are attributes not supported 425 if(tag.getAttributeType()==TagLibTag.ATTRIBUTE_TYPE_FIXED && count<attributesScope.size()){ 426 Collection.Key[] keys = attributesScope.keys(); 427 for(int i=0;i<keys.length;i++){ 428 if(tag.getAttribute(keys[i].getLowerString())==null) 429 throw new ApplicationException("attribute ["+keys[i].getString()+"] is not supported for tag ["+tagName+"]"); 430 } 431 432 //Attribute susi is not allowed for tag cfmail 433 } 434 } 435 } 436 437 private static String createMessage(String type, Object value) { 438 if(value instanceof String) return "can't cast String ["+value+"] to a value of type ["+type+"]"; 439 else if(value!=null) return "can't cast Object type ["+Type.getName(value)+"] to a value of type ["+type+"]"; 440 else return "can't cast Null value to value of type ["+type+"]"; 441 442 } 443 444 445 private static TagLibTag getAttributeRequirments(ComponentPro cfc, boolean runtime) throws ExpressionException { 446 Struct meta=null; 447 //try { 448 //meta = Caster.toStruct(cfc.get(Component.ACCESS_PRIVATE, METADATA),null,false); 449 Member mem = ComponentUtil.toComponentAccess(cfc).getMember(Component.ACCESS_PRIVATE, METADATA,true,false); 450 if(mem!=null)meta = Caster.toStruct(mem.getValue(),null,false); 451 //}catch (PageException e) {e.printStackTrace();} 452 if(meta==null) return null; 453 454 TagLibTag tag=new TagLibTag(null); 455 // TAG 456 457 // type 458 459 String type=Caster.toString(meta.get(ATTRIBUTE_TYPE,"dynamic"),"dynamic"); 460 461 if("fixed".equalsIgnoreCase(type))tag.setAttributeType(TagLibTag.ATTRIBUTE_TYPE_FIXED); 462 //else if("mixed".equalsIgnoreCase(type))tag.setAttributeType(TagLibTag.ATTRIBUTE_TYPE_MIXED); 463 //else if("noname".equalsIgnoreCase(type))tag.setAttributeType(TagLibTag.ATTRIBUTE_TYPE_NONAME); 464 else tag.setAttributeType(TagLibTag.ATTRIBUTE_TYPE_DYNAMIC); 465 466 if(!runtime){ 467 // hint 468 String hint=Caster.toString(meta.get(KeyImpl.HINT,null),null); 469 if(!StringUtil.isEmpty(hint))tag.setDescription(hint); 470 471 // parseBody 472 boolean rtexprvalue=Caster.toBooleanValue(meta.get(PARSE_BODY,Boolean.FALSE),false); 473 tag.setParseBody(rtexprvalue); 474 } 475 476 // ATTRIBUTES 477 Struct attributes=Caster.toStruct(meta.get(ATTRIBUTES,null),null,false); 478 if(attributes!=null) { 479 Iterator it = attributes.entrySet().iterator(); 480 Map.Entry entry; 481 TagLibTagAttr attr; 482 Struct sct; 483 String name; 484 Object defaultValue; 485 while(it.hasNext()){ 486 entry=(Entry) it.next(); 487 name=Caster.toString(entry.getKey(),null); 488 if(StringUtil.isEmpty(name)) continue; 489 attr=new TagLibTagAttr(tag); 490 attr.setName(name); 491 492 sct=Caster.toStruct(entry.getValue(),null,false); 493 if(sct!=null){ 494 attr.setRequired(Caster.toBooleanValue(sct.get(KeyImpl.REQUIRED,Boolean.FALSE),false)); 495 attr.setType(Caster.toString(sct.get(KeyImpl.TYPE,"any"),"any")); 496 497 defaultValue= sct.get(KeyImpl.DEFAULT,null); 498 if(defaultValue!=null)attr.setDefaultValue(defaultValue); 499 500 501 if(!runtime){ 502 attr.setDescription(Caster.toString(sct.get(KeyImpl.HINT,null),null)); 503 attr.setRtexpr(Caster.toBooleanValue(sct.get(RT_EXPR_VALUE,Boolean.TRUE),true)); 504 } 505 } 506 tag.setAttribute(attr); 507 508 } 509 } 510 return tag; 511 } 512 513 private int cfcEndTag() throws PageException { 514 515 boolean exeAgain = false; 516 try{ 517 String output=null; 518 Object rtn=Boolean.FALSE; 519 520 521 if(cfc.contains(pageContext, ON_END_TAG)){ 522 try { 523 output=bodyContent.getString(); 524 bodyContent.clearBody(); 525 //rtn=cfc.call(pageContext, ON_END_TAG, new Object[]{attributesScope,pageContext.variablesScope(),output}); 526 527 Struct args=new StructImpl(StructImpl.TYPE_LINKED); 528 args.set(ATTRIBUTES, attributesScope); 529 setCaller(pageContext, args); 530 args.set(GENERATED_CONTENT, output); 531 rtn=cfc.callWithNamedValues(pageContext, ON_END_TAG, args); 532 533 534 535 } 536 finally { 537 writeEnclosingWriter(); 538 } 539 } 540 else writeEnclosingWriter(); 541 542 exeAgain= Caster.toBooleanValue(rtn,false); 543 } 544 catch(Throwable t){ 545 isEndTag=true; 546 throw Caster.toPageException(t); 547 } 548 return exeAgain?EVAL_BODY_BUFFERED:SKIP_BODY; 549 550 } 551 552 public void _doCFCCatch(Throwable t, String source) throws PageException { 553 writeEnclosingWriter(); 554 555 // remove PageServletException wrap 556 if(t instanceof PageServletException) { 557 PageServletException pse=(PageServletException)t; 558 t=pse.getPageException(); 559 } 560 561 // abort 562 try { 563 if(railo.runtime.exp.Abort.isAbort(t)){ 564 if(bodyContent!=null){ 565 bodyContent.writeOut(bodyContent.getEnclosingWriter()); 566 bodyContent.clearBuffer(); 567 } 568 throw Caster.toPageException(t); 569 } 570 } 571 catch(IOException ioe){ 572 throw Caster.toPageException(ioe); 573 } 574 575 576 577 try { 578 if(cfc.contains(pageContext, ON_ERROR)){ 579 PageException pe = Caster.toPageException(t); 580 //Object rtn=cfc.call(pageContext, ON_ERROR, new Object[]{pe.getCatchBlock(pageContext),source}); 581 582 Struct args=new StructImpl(StructImpl.TYPE_LINKED); 583 args.set(CFCATCH, pe.getCatchBlock(pageContext)); 584 args.set(SOURCE, source); 585 Object rtn=cfc.callWithNamedValues(pageContext, ON_ERROR, args); 586 587 if(Caster.toBooleanValue(rtn,false)) 588 throw t; 589 } 590 else throw t; 591 } 592 catch(Throwable th) { 593 writeEnclosingWriter(); 594 _doCFCFinally(); 595 throw Caster.toPageException(th); 596 } 597 writeEnclosingWriter(); 598 } 599 600 private void _doCFCFinally() { 601 if(cfc.contains(pageContext, ON_FINALLY)){ 602 try { 603 cfc.call(pageContext, ON_FINALLY, ArrayUtil.OBJECT_EMPTY); 604 } 605 catch (PageException pe) { 606 throw new PageRuntimeException(pe); 607 } 608 finally{ 609 writeEnclosingWriter(); 610 } 611 } 612 } 613 614 615 private void writeEnclosingWriter() { 616 if(bodyContent!=null){ 617 try { 618 String output = bodyContent.getString(); 619 bodyContent.clearBody(); 620 bodyContent.getEnclosingWriter().write(output); 621 } 622 catch (IOException e) { 623 //throw Caster.toPageException(e); 624 } 625 } 626 } 627 628 629 630 /** 631 * sets if tag has a body or not 632 * @param hasBody 633 */ 634 public void hasBody(boolean hasBody) { 635 this.hasBody=hasBody; 636 } 637 638 /** 639 * @return Returns the appendix. 640 */ 641 public String getAppendix() { 642 return appendix; 643 } 644 645 /** 646 * @return return thistag 647 */ 648 public Struct getThis() { 649 if(isCFCBasedCustomTag()){ 650 return cfc; 651 } 652 return thistagScope; 653 } 654 655 /** 656 * @return return thistag 657 */ 658 public Struct getCallerScope() { 659 return callerScope; 660 } 661 662 /** 663 * @return return thistag 664 */ 665 public Struct getAttributesScope() { 666 return attributesScope; 667 } 668 669 /** 670 * @return the ctVariablesScope 671 */ 672 public Struct getVariablesScope() { 673 if(isCFCBasedCustomTag()) { 674 return cfc.getComponentScope(); 675 } 676 return ctVariablesScope; 677 } 678 679 /** 680 * @return the cfc 681 */ 682 public Component getComponent() { 683 return cfc; 684 } 685 686 public boolean isCFCBasedCustomTag() { 687 return getSource().isCFC(); 688 } 689 690 private InitFile getSource() { 691 if(source==null){ 692 try { 693 source=initFile(pageContext); 694 } catch (PageException e) { 695 e.printStackTrace(); 696 } 697 } 698 return source; 699 } 700 701 /*class InitFile { 702 PageSource ps; 703 String filename; 704 boolean isCFC; 705 706 public InitFile(PageSource ps,String filename,boolean isCFC){ 707 this.ps=ps; 708 this.filename=filename; 709 this.isCFC=isCFC; 710 } 711 }*/ 712 713 714 }