001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.impl.converter; 018 019import java.io.IOException; 020import java.util.ArrayList; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.concurrent.ConcurrentHashMap; 027import java.util.concurrent.ConcurrentMap; 028import java.util.concurrent.CopyOnWriteArrayList; 029import java.util.concurrent.ExecutionException; 030import java.util.concurrent.atomic.AtomicLong; 031 032import org.apache.camel.CamelExecutionException; 033import org.apache.camel.Exchange; 034import org.apache.camel.NoFactoryAvailableException; 035import org.apache.camel.NoTypeConversionAvailableException; 036import org.apache.camel.TypeConversionException; 037import org.apache.camel.TypeConverter; 038import org.apache.camel.spi.FactoryFinder; 039import org.apache.camel.spi.Injector; 040import org.apache.camel.spi.PackageScanClassResolver; 041import org.apache.camel.spi.TypeConverterAware; 042import org.apache.camel.spi.TypeConverterLoader; 043import org.apache.camel.spi.TypeConverterRegistry; 044import org.apache.camel.support.ServiceSupport; 045import org.apache.camel.util.LRUSoftCache; 046import org.apache.camel.util.MessageHelper; 047import org.apache.camel.util.ObjectHelper; 048import org.slf4j.Logger; 049import org.slf4j.LoggerFactory; 050 051/** 052 * Base implementation of a type converter registry used for 053 * <a href="http://camel.apache.org/type-converter.html">type converters</a> in Camel. 054 * 055 * @version 056 */ 057public abstract class BaseTypeConverterRegistry extends ServiceSupport implements TypeConverter, TypeConverterRegistry { 058 protected final Logger log = LoggerFactory.getLogger(getClass()); 059 protected final ConcurrentMap<TypeMapping, TypeConverter> typeMappings = new ConcurrentHashMap<TypeMapping, TypeConverter>(); 060 // for misses use a soft reference cache map, as the classes may be un-deployed at runtime 061 protected final LRUSoftCache<TypeMapping, TypeMapping> misses = new LRUSoftCache<TypeMapping, TypeMapping>(1000); 062 protected final List<TypeConverterLoader> typeConverterLoaders = new ArrayList<TypeConverterLoader>(); 063 protected final List<FallbackTypeConverter> fallbackConverters = new CopyOnWriteArrayList<FallbackTypeConverter>(); 064 protected final PackageScanClassResolver resolver; 065 protected Injector injector; 066 protected final FactoryFinder factoryFinder; 067 protected final Statistics statistics = new UtilizationStatistics(); 068 protected final AtomicLong noopCounter = new AtomicLong(); 069 protected final AtomicLong attemptCounter = new AtomicLong(); 070 protected final AtomicLong missCounter = new AtomicLong(); 071 protected final AtomicLong hitCounter = new AtomicLong(); 072 protected final AtomicLong failedCounter = new AtomicLong(); 073 074 public BaseTypeConverterRegistry(PackageScanClassResolver resolver, Injector injector, FactoryFinder factoryFinder) { 075 this.resolver = resolver; 076 this.injector = injector; 077 this.factoryFinder = factoryFinder; 078 this.typeConverterLoaders.add(new AnnotationTypeConverterLoader(resolver)); 079 080 // add to string first as it will then be last in the last as to string can nearly 081 // always convert something to a string so we want it only as the last resort 082 // ToStringTypeConverter should NOT allow to be promoted 083 addFallbackTypeConverter(new ToStringTypeConverter(), false); 084 // enum is okay to be promoted 085 addFallbackTypeConverter(new EnumTypeConverter(), true); 086 // arrays is okay to be promoted 087 addFallbackTypeConverter(new ArrayTypeConverter(), true); 088 // and future should also not allowed to be promoted 089 addFallbackTypeConverter(new FutureTypeConverter(this), false); 090 // add sync processor to async processor converter is to be promoted 091 addFallbackTypeConverter(new AsyncProcessorTypeConverter(), true); 092 } 093 094 public List<TypeConverterLoader> getTypeConverterLoaders() { 095 return typeConverterLoaders; 096 } 097 098 @Override 099 public <T> T convertTo(Class<T> type, Object value) { 100 return convertTo(type, null, value); 101 } 102 103 @SuppressWarnings("unchecked") 104 @Override 105 public <T> T convertTo(Class<T> type, Exchange exchange, Object value) { 106 if (!isRunAllowed()) { 107 throw new IllegalStateException(this + " is not started"); 108 } 109 110 Object answer; 111 try { 112 answer = doConvertTo(type, exchange, value, false); 113 } catch (Exception e) { 114 if (statistics.isStatisticsEnabled()) { 115 failedCounter.incrementAndGet(); 116 } 117 // if its a ExecutionException then we have rethrow it as its not due to failed conversion 118 // this is special for FutureTypeConverter 119 boolean execution = ObjectHelper.getException(ExecutionException.class, e) != null 120 || ObjectHelper.getException(CamelExecutionException.class, e) != null; 121 if (execution) { 122 throw ObjectHelper.wrapCamelExecutionException(exchange, e); 123 } 124 125 // error occurred during type conversion 126 if (e instanceof TypeConversionException) { 127 throw (TypeConversionException) e; 128 } else { 129 throw createTypeConversionException(exchange, type, value, e); 130 } 131 } 132 if (answer == Void.TYPE) { 133 if (statistics.isStatisticsEnabled()) { 134 missCounter.incrementAndGet(); 135 } 136 // Could not find suitable conversion 137 return null; 138 } else { 139 if (statistics.isStatisticsEnabled()) { 140 hitCounter.incrementAndGet(); 141 } 142 return (T) answer; 143 } 144 } 145 146 @Override 147 public <T> T mandatoryConvertTo(Class<T> type, Object value) throws NoTypeConversionAvailableException { 148 return mandatoryConvertTo(type, null, value); 149 } 150 151 @SuppressWarnings("unchecked") 152 @Override 153 public <T> T mandatoryConvertTo(Class<T> type, Exchange exchange, Object value) throws NoTypeConversionAvailableException { 154 if (!isRunAllowed()) { 155 throw new IllegalStateException(this + " is not started"); 156 } 157 158 Object answer; 159 try { 160 answer = doConvertTo(type, exchange, value, false); 161 } catch (Exception e) { 162 if (statistics.isStatisticsEnabled()) { 163 failedCounter.incrementAndGet(); 164 } 165 // error occurred during type conversion 166 if (e instanceof TypeConversionException) { 167 throw (TypeConversionException) e; 168 } else { 169 throw createTypeConversionException(exchange, type, value, e); 170 } 171 } 172 if (answer == Void.TYPE || value == null) { 173 if (statistics.isStatisticsEnabled()) { 174 missCounter.incrementAndGet(); 175 } 176 // Could not find suitable conversion 177 throw new NoTypeConversionAvailableException(value, type); 178 } else { 179 if (statistics.isStatisticsEnabled()) { 180 hitCounter.incrementAndGet(); 181 } 182 return (T) answer; 183 } 184 } 185 186 @Override 187 public <T> T tryConvertTo(Class<T> type, Object value) { 188 return tryConvertTo(type, null, value); 189 } 190 191 @SuppressWarnings("unchecked") 192 @Override 193 public <T> T tryConvertTo(Class<T> type, Exchange exchange, Object value) { 194 if (!isRunAllowed()) { 195 return null; 196 } 197 198 Object answer; 199 try { 200 answer = doConvertTo(type, exchange, value, true); 201 } catch (Exception e) { 202 if (statistics.isStatisticsEnabled()) { 203 failedCounter.incrementAndGet(); 204 } 205 return null; 206 } 207 if (answer == Void.TYPE) { 208 // Could not find suitable conversion 209 if (statistics.isStatisticsEnabled()) { 210 missCounter.incrementAndGet(); 211 } 212 return null; 213 } else { 214 if (statistics.isStatisticsEnabled()) { 215 hitCounter.incrementAndGet(); 216 } 217 return (T) answer; 218 } 219 } 220 221 protected Object doConvertTo(final Class<?> type, final Exchange exchange, final Object value, final boolean tryConvert) { 222 if (log.isTraceEnabled()) { 223 log.trace("Converting {} -> {} with value: {}", 224 new Object[]{value == null ? "null" : value.getClass().getCanonicalName(), 225 type.getCanonicalName(), value}); 226 } 227 228 if (value == null) { 229 // no type conversion was needed 230 if (statistics.isStatisticsEnabled()) { 231 noopCounter.incrementAndGet(); 232 } 233 // lets avoid NullPointerException when converting to boolean for null values 234 if (boolean.class.isAssignableFrom(type)) { 235 return Boolean.FALSE; 236 } 237 return null; 238 } 239 240 // same instance type 241 if (type.isInstance(value)) { 242 // no type conversion was needed 243 if (statistics.isStatisticsEnabled()) { 244 noopCounter.incrementAndGet(); 245 } 246 return type.cast(value); 247 } 248 249 // special for NaN numbers, which we can only convert for floating numbers 250 if (ObjectHelper.isNaN(value)) { 251 // no type conversion was needed 252 if (statistics.isStatisticsEnabled()) { 253 noopCounter.incrementAndGet(); 254 } 255 if (Float.class.isAssignableFrom(type)) { 256 return Float.NaN; 257 } else if (Double.class.isAssignableFrom(type)) { 258 return Double.NaN; 259 } else { 260 // we cannot convert the NaN 261 return Void.TYPE; 262 } 263 } 264 265 // okay we need to attempt to convert 266 if (statistics.isStatisticsEnabled()) { 267 attemptCounter.incrementAndGet(); 268 } 269 270 // check if we have tried it before and if its a miss 271 TypeMapping key = new TypeMapping(type, value.getClass()); 272 if (misses.containsKey(key)) { 273 // we have tried before but we cannot convert this one 274 return Void.TYPE; 275 } 276 277 // try to find a suitable type converter 278 TypeConverter converter = getOrFindTypeConverter(key); 279 if (converter != null) { 280 log.trace("Using converter: {} to convert {}", converter, key); 281 Object rc; 282 if (tryConvert) { 283 rc = converter.tryConvertTo(type, exchange, value); 284 } else { 285 rc = converter.convertTo(type, exchange, value); 286 } 287 if (rc == null && converter.allowNull()) { 288 return null; 289 } else if (rc != null) { 290 return rc; 291 } 292 } 293 294 // not found with that type then if it was a primitive type then try again with the wrapper type 295 if (type.isPrimitive()) { 296 Class<?> primitiveType = ObjectHelper.convertPrimitiveTypeToWrapperType(type); 297 if (primitiveType != type) { 298 Class<?> fromType = value.getClass(); 299 TypeConverter tc = getOrFindTypeConverter(new TypeMapping(primitiveType, fromType)); 300 if (tc != null) { 301 // add the type as a known type converter as we can convert from primitive to object converter 302 addTypeConverter(type, fromType, tc); 303 Object rc; 304 if (tryConvert) { 305 rc = tc.tryConvertTo(primitiveType, exchange, value); 306 } else { 307 rc = tc.convertTo(primitiveType, exchange, value); 308 } 309 if (rc == null && tc.allowNull()) { 310 return null; 311 } else if (rc != null) { 312 return rc; 313 } 314 } 315 } 316 } 317 318 // fallback converters 319 for (FallbackTypeConverter fallback : fallbackConverters) { 320 TypeConverter tc = fallback.getFallbackTypeConverter(); 321 Object rc; 322 if (tryConvert) { 323 rc = tc.tryConvertTo(type, exchange, value); 324 } else { 325 rc = tc.convertTo(type, exchange, value); 326 } 327 if (rc == null && tc.allowNull()) { 328 return null; 329 } 330 331 if (Void.TYPE.equals(rc)) { 332 // it cannot be converted so give up 333 return Void.TYPE; 334 } 335 336 if (rc != null) { 337 // if fallback can promote then let it be promoted to a first class type converter 338 if (fallback.isCanPromote()) { 339 // add it as a known type converter since we found a fallback that could do it 340 if (log.isDebugEnabled()) { 341 log.debug("Promoting fallback type converter as a known type converter to convert from: {} to: {} for the fallback converter: {}", 342 new Object[]{type.getCanonicalName(), value.getClass().getCanonicalName(), fallback.getFallbackTypeConverter()}); 343 } 344 addTypeConverter(type, value.getClass(), fallback.getFallbackTypeConverter()); 345 } 346 347 if (log.isTraceEnabled()) { 348 log.trace("Fallback type converter {} converted type from: {} to: {}", 349 new Object[]{fallback.getFallbackTypeConverter(), 350 type.getCanonicalName(), value.getClass().getCanonicalName()}); 351 } 352 353 // return converted value 354 return rc; 355 } 356 } 357 358 if (!tryConvert) { 359 // Could not find suitable conversion, so remember it 360 // do not register misses for try conversions 361 misses.put(key, key); 362 } 363 364 // Could not find suitable conversion, so return Void to indicate not found 365 return Void.TYPE; 366 } 367 368 @Override 369 public void addTypeConverter(Class<?> toType, Class<?> fromType, TypeConverter typeConverter) { 370 log.trace("Adding type converter: {}", typeConverter); 371 TypeMapping key = new TypeMapping(toType, fromType); 372 TypeConverter converter = typeMappings.get(key); 373 // only override it if its different 374 // as race conditions can lead to many threads trying to promote the same fallback converter 375 if (typeConverter != converter) { 376 if (converter != null) { 377 log.warn("Overriding type converter from: " + converter + " to: " + typeConverter); 378 } 379 typeMappings.put(key, typeConverter); 380 // remove any previous misses, as we added the new type converter 381 misses.remove(key); 382 } 383 } 384 385 @Override 386 public boolean removeTypeConverter(Class<?> toType, Class<?> fromType) { 387 log.trace("Removing type converter from: {} to: {}", fromType, toType); 388 TypeMapping key = new TypeMapping(toType, fromType); 389 TypeConverter converter = typeMappings.remove(key); 390 if (converter != null) { 391 typeMappings.remove(key); 392 misses.remove(key); 393 } 394 return converter != null; 395 } 396 397 @Override 398 public void addFallbackTypeConverter(TypeConverter typeConverter, boolean canPromote) { 399 log.trace("Adding fallback type converter: {} which can promote: {}", typeConverter, canPromote); 400 401 // add in top of fallback as the toString() fallback will nearly always be able to convert 402 // the last one which is add to the FallbackTypeConverter will be called at the first place 403 fallbackConverters.add(0, new FallbackTypeConverter(typeConverter, canPromote)); 404 if (typeConverter instanceof TypeConverterAware) { 405 TypeConverterAware typeConverterAware = (TypeConverterAware) typeConverter; 406 typeConverterAware.setTypeConverter(this); 407 } 408 } 409 410 public TypeConverter getTypeConverter(Class<?> toType, Class<?> fromType) { 411 TypeMapping key = new TypeMapping(toType, fromType); 412 return typeMappings.get(key); 413 } 414 415 @Override 416 public Injector getInjector() { 417 return injector; 418 } 419 420 @Override 421 public void setInjector(Injector injector) { 422 this.injector = injector; 423 } 424 425 public Set<Class<?>> getFromClassMappings() { 426 Set<Class<?>> answer = new HashSet<Class<?>>(); 427 for (TypeMapping mapping : typeMappings.keySet()) { 428 answer.add(mapping.getFromType()); 429 } 430 return answer; 431 } 432 433 public Map<Class<?>, TypeConverter> getToClassMappings(Class<?> fromClass) { 434 Map<Class<?>, TypeConverter> answer = new HashMap<Class<?>, TypeConverter>(); 435 for (Map.Entry<TypeMapping, TypeConverter> entry : typeMappings.entrySet()) { 436 TypeMapping mapping = entry.getKey(); 437 if (mapping.isApplicable(fromClass)) { 438 answer.put(mapping.getToType(), entry.getValue()); 439 } 440 } 441 return answer; 442 } 443 444 public Map<TypeMapping, TypeConverter> getTypeMappings() { 445 return typeMappings; 446 } 447 448 protected <T> TypeConverter getOrFindTypeConverter(TypeMapping key) { 449 TypeConverter converter = typeMappings.get(key); 450 if (converter == null) { 451 // converter not found, try to lookup then 452 converter = lookup(key.getToType(), key.getFromType()); 453 if (converter != null) { 454 typeMappings.putIfAbsent(key, converter); 455 } 456 } 457 return converter; 458 } 459 460 @Override 461 public TypeConverter lookup(Class<?> toType, Class<?> fromType) { 462 return doLookup(toType, fromType, false); 463 } 464 465 protected TypeConverter doLookup(Class<?> toType, Class<?> fromType, boolean isSuper) { 466 467 if (fromType != null) { 468 // lets try if there is a direct match 469 TypeConverter converter = getTypeConverter(toType, fromType); 470 if (converter != null) { 471 return converter; 472 } 473 474 // try the interfaces 475 for (Class<?> type : fromType.getInterfaces()) { 476 converter = getTypeConverter(toType, type); 477 if (converter != null) { 478 return converter; 479 } 480 } 481 482 // try super then 483 Class<?> fromSuperClass = fromType.getSuperclass(); 484 if (fromSuperClass != null && !fromSuperClass.equals(Object.class)) { 485 converter = doLookup(toType, fromSuperClass, true); 486 if (converter != null) { 487 return converter; 488 } 489 } 490 } 491 492 // only do these tests as fallback and only on the target type (eg not on its super) 493 if (!isSuper) { 494 if (fromType != null && !fromType.equals(Object.class)) { 495 496 // lets try classes derived from this toType 497 Set<Map.Entry<TypeMapping, TypeConverter>> entries = typeMappings.entrySet(); 498 for (Map.Entry<TypeMapping, TypeConverter> entry : entries) { 499 TypeMapping key = entry.getKey(); 500 Class<?> aToType = key.getToType(); 501 if (toType.isAssignableFrom(aToType)) { 502 Class<?> aFromType = key.getFromType(); 503 // skip Object based we do them last 504 if (!aFromType.equals(Object.class) && aFromType.isAssignableFrom(fromType)) { 505 return entry.getValue(); 506 } 507 } 508 } 509 510 // lets test for Object based converters as last resort 511 TypeConverter converter = getTypeConverter(toType, Object.class); 512 if (converter != null) { 513 return converter; 514 } 515 } 516 } 517 518 // none found 519 return null; 520 } 521 522 public List<Class<?>[]> listAllTypeConvertersFromTo() { 523 List<Class<?>[]> answer = new ArrayList<Class<?>[]>(typeMappings.size()); 524 for (TypeMapping mapping : typeMappings.keySet()) { 525 answer.add(new Class<?>[]{mapping.getFromType(), mapping.getToType()}); 526 } 527 return answer; 528 } 529 530 /** 531 * Loads the core type converters which is mandatory to use Camel 532 */ 533 public void loadCoreTypeConverters() throws Exception { 534 // load all the type converters from camel-core 535 CoreTypeConverterLoader core = new CoreTypeConverterLoader(); 536 core.load(this); 537 } 538 539 /** 540 * Checks if the registry is loaded and if not lazily load it 541 */ 542 protected void loadTypeConverters() throws Exception { 543 for (TypeConverterLoader typeConverterLoader : getTypeConverterLoaders()) { 544 typeConverterLoader.load(this); 545 } 546 547 // lets try load any other fallback converters 548 try { 549 loadFallbackTypeConverters(); 550 } catch (NoFactoryAvailableException e) { 551 // ignore its fine to have none 552 } 553 } 554 555 protected void loadFallbackTypeConverters() throws IOException, ClassNotFoundException { 556 List<TypeConverter> converters = factoryFinder.newInstances("FallbackTypeConverter", getInjector(), TypeConverter.class); 557 for (TypeConverter converter : converters) { 558 addFallbackTypeConverter(converter, false); 559 } 560 } 561 562 protected TypeConversionException createTypeConversionException(Exchange exchange, Class<?> type, Object value, Throwable cause) { 563 Object body; 564 // extract the body for logging which allows to limit the message body in the exception/stacktrace 565 // and also can be used to turn off logging sensitive message data 566 if (exchange != null) { 567 body = MessageHelper.extractValueForLogging(value, exchange.getIn()); 568 } else { 569 body = value; 570 } 571 return new TypeConversionException(body, type, cause); 572 } 573 574 @Override 575 public Statistics getStatistics() { 576 return statistics; 577 } 578 579 @Override 580 public int size() { 581 return typeMappings.size(); 582 } 583 584 @Override 585 protected void doStart() throws Exception { 586 // noop 587 } 588 589 @Override 590 protected void doStop() throws Exception { 591 // log utilization statistics when stopping, including mappings 592 if (statistics.isStatisticsEnabled()) { 593 String info = statistics.toString(); 594 info += String.format(" mappings[total=%s, misses=%s]", typeMappings.size(), misses.size()); 595 log.info(info); 596 } 597 598 typeMappings.clear(); 599 misses.clear(); 600 statistics.reset(); 601 } 602 603 /** 604 * Represents utilization statistics 605 */ 606 private final class UtilizationStatistics implements Statistics { 607 608 private boolean statisticsEnabled; 609 610 @Override 611 public long getNoopCounter() { 612 return noopCounter.get(); 613 } 614 615 @Override 616 public long getAttemptCounter() { 617 return attemptCounter.get(); 618 } 619 620 @Override 621 public long getHitCounter() { 622 return hitCounter.get(); 623 } 624 625 @Override 626 public long getMissCounter() { 627 return missCounter.get(); 628 } 629 630 @Override 631 public long getFailedCounter() { 632 return failedCounter.get(); 633 } 634 635 @Override 636 public void reset() { 637 noopCounter.set(0); 638 attemptCounter.set(0); 639 hitCounter.set(0); 640 missCounter.set(0); 641 failedCounter.set(0); 642 } 643 644 @Override 645 public boolean isStatisticsEnabled() { 646 return statisticsEnabled; 647 } 648 649 @Override 650 public void setStatisticsEnabled(boolean statisticsEnabled) { 651 this.statisticsEnabled = statisticsEnabled; 652 } 653 654 @Override 655 public String toString() { 656 return String.format("TypeConverterRegistry utilization[noop=%s, attempts=%s, hits=%s, misses=%s, failures=%s]", 657 getNoopCounter(), getAttemptCounter(), getHitCounter(), getMissCounter(), getFailedCounter()); 658 } 659 } 660 661 /** 662 * Represents a mapping from one type (which can be null) to another 663 */ 664 protected static class TypeMapping { 665 private final Class<?> toType; 666 private final Class<?> fromType; 667 668 TypeMapping(Class<?> toType, Class<?> fromType) { 669 this.toType = toType; 670 this.fromType = fromType; 671 } 672 673 public Class<?> getFromType() { 674 return fromType; 675 } 676 677 public Class<?> getToType() { 678 return toType; 679 } 680 681 @Override 682 public boolean equals(Object object) { 683 if (object instanceof TypeMapping) { 684 TypeMapping that = (TypeMapping) object; 685 return ObjectHelper.equal(this.fromType, that.fromType) 686 && ObjectHelper.equal(this.toType, that.toType); 687 } 688 return false; 689 } 690 691 @Override 692 public int hashCode() { 693 int answer = toType.hashCode(); 694 if (fromType != null) { 695 answer *= 37 + fromType.hashCode(); 696 } 697 return answer; 698 } 699 700 @Override 701 public String toString() { 702 return "[" + fromType + "=>" + toType + "]"; 703 } 704 705 public boolean isApplicable(Class<?> fromClass) { 706 return fromType.isAssignableFrom(fromClass); 707 } 708 } 709 710 /** 711 * Represents a fallback type converter 712 */ 713 protected static class FallbackTypeConverter { 714 private final boolean canPromote; 715 private final TypeConverter fallbackTypeConverter; 716 717 FallbackTypeConverter(TypeConverter fallbackTypeConverter, boolean canPromote) { 718 this.canPromote = canPromote; 719 this.fallbackTypeConverter = fallbackTypeConverter; 720 } 721 722 public boolean isCanPromote() { 723 return canPromote; 724 } 725 726 public TypeConverter getFallbackTypeConverter() { 727 return fallbackTypeConverter; 728 } 729 } 730}