001 /* 002 * Copyright (c) 2007-2014 Concurrent, Inc. All Rights Reserved. 003 * 004 * Project and contact information: http://www.cascading.org/ 005 * 006 * This file is part of the Cascading project. 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020 021 package cascading.tap; 022 023 import java.io.IOException; 024 import java.io.Serializable; 025 import java.util.Set; 026 027 import cascading.flow.Flow; 028 import cascading.flow.FlowElement; 029 import cascading.flow.FlowException; 030 import cascading.flow.FlowProcess; 031 import cascading.flow.planner.Scope; 032 import cascading.management.annotation.Property; 033 import cascading.management.annotation.PropertyDescription; 034 import cascading.management.annotation.PropertySanitizer; 035 import cascading.management.annotation.Visibility; 036 import cascading.pipe.Pipe; 037 import cascading.property.ConfigDef; 038 import cascading.scheme.Scheme; 039 import cascading.tuple.Fields; 040 import cascading.tuple.FieldsResolverException; 041 import cascading.tuple.Tuple; 042 import cascading.tuple.TupleEntryCollector; 043 import cascading.tuple.TupleEntryIterator; 044 import cascading.util.TraceUtil; 045 import cascading.util.Traceable; 046 import cascading.util.Util; 047 048 /** 049 * A Tap represents the physical data source or sink in a connected {@link cascading.flow.Flow}. 050 * </p> 051 * That is, a source Tap is the head end of a connected {@link Pipe} and {@link Tuple} stream, and 052 * a sink Tap is the tail end. Kinds of Tap types are used to manage files from a local disk, 053 * distributed disk, remote storage like Amazon S3, or via FTP. It simply abstracts 054 * out the complexity of connecting to these types of data sources. 055 * <p/> 056 * A Tap takes a {@link Scheme} instance, which is used to identify the type of resource (text file, binary file, etc). 057 * A Tap is responsible for how the resource is reached. 058 * <p/> 059 * By default when planning a Flow, Tap equality is a function of the {@link #getIdentifier()} and {@link #getScheme()} 060 * values. That is, two Tap instances are the same Tap instance if they sink/source the same resource and sink/source 061 * the same fields. 062 * <p/> 063 * Some more advanced taps, like a database tap, may need to extend equality to include any filtering, like the 064 * {@code where} clause in a SQL statement so two taps reading from the same SQL table aren't considered equal. 065 * <p/> 066 * Taps are also used to determine dependencies between two or more {@link Flow} instances when used with a 067 * {@link cascading.cascade.Cascade}. In that case the {@link #getFullIdentifier(Object)} value is used and the Scheme 068 * is ignored. 069 */ 070 public abstract class Tap<Config, Input, Output> implements FlowElement, Serializable, Traceable 071 { 072 /** Field scheme */ 073 private Scheme<Config, Input, Output, ?, ?> scheme; 074 075 /** Field mode */ 076 SinkMode sinkMode = SinkMode.KEEP; 077 078 private ConfigDef configDef; 079 080 private ConfigDef processConfigDef; 081 082 /** Field id */ 083 private final String id = Util.createUniqueID(); // 3.0 planner relies on this being consistent 084 /** Field trace */ 085 private String trace = TraceUtil.captureDebugTrace( this ); // see TraceUtil.setTrace() to override 086 087 /** 088 * Convenience function to make an array of Tap instances. 089 * 090 * @param taps of type Tap 091 * @return Tap array 092 */ 093 public static Tap[] taps( Tap... taps ) 094 { 095 return taps; 096 } 097 098 /** 099 * Creates and returns a unique ID for the given Tap, this value is cached and may be used to uniquely identify 100 * the Tap instance in properties files etc. 101 * <p/> 102 * This value is generally reproducible assuming the Tap identifier and the Scheme source and sink Fields remain consistent. 103 * 104 * @param tap of type Tap 105 * @return of type String 106 */ 107 public static synchronized String id( Tap tap ) 108 { 109 if( tap instanceof DecoratorTap ) 110 return id( ( (DecoratorTap) tap ).getOriginal() ); 111 112 return tap.id; 113 } 114 115 protected Tap() 116 { 117 } 118 119 protected Tap( Scheme<Config, Input, Output, ?, ?> scheme ) 120 { 121 this.setScheme( scheme ); 122 } 123 124 protected Tap( Scheme<Config, Input, Output, ?, ?> scheme, SinkMode sinkMode ) 125 { 126 this.setScheme( scheme ); 127 this.sinkMode = sinkMode; 128 } 129 130 protected void setScheme( Scheme<Config, Input, Output, ?, ?> scheme ) 131 { 132 this.scheme = scheme; 133 } 134 135 /** 136 * Method getScheme returns the scheme of this Tap object. 137 * 138 * @return the scheme (type Scheme) of this Tap object. 139 */ 140 public Scheme<Config, Input, Output, ?, ?> getScheme() 141 { 142 return scheme; 143 } 144 145 @Override 146 public String getTrace() 147 { 148 return trace; 149 } 150 151 /** 152 * Method flowInit allows this Tap instance to initialize itself in context of the given {@link cascading.flow.Flow} instance. 153 * This method is guaranteed to be called before the Flow is started and the 154 * {@link cascading.flow.FlowListener#onStarting(cascading.flow.Flow)} event is fired. 155 * <p/> 156 * This method will be called once per Flow, and before {@link #sourceConfInit(cascading.flow.FlowProcess, Object)} and 157 * {@link #sinkConfInit(cascading.flow.FlowProcess, Object)} methods. 158 * 159 * @param flow of type Flow 160 */ 161 public void flowConfInit( Flow<Config> flow ) 162 { 163 164 } 165 166 /** 167 * Method sourceConfInit initializes this instance as a source. 168 * <p/> 169 * This method maybe called more than once if this Tap instance is used outside the scope of a {@link cascading.flow.Flow} 170 * instance or if it participates in multiple times in a given Flow or across different Flows in 171 * a {@link cascading.cascade.Cascade}. 172 * <p/> 173 * In the context of a Flow, it will be called after 174 * {@link cascading.flow.FlowListener#onStarting(cascading.flow.Flow)} 175 * <p/> 176 * Note that no resources or services should be modified by this method. 177 * 178 * @param flowProcess of type FlowProcess 179 * @param conf of type Config 180 */ 181 public void sourceConfInit( FlowProcess<Config> flowProcess, Config conf ) 182 { 183 getScheme().sourceConfInit( flowProcess, this, conf ); 184 } 185 186 /** 187 * Method sinkConfInit initializes this instance as a sink. 188 * <p/> 189 * This method maybe called more than once if this Tap instance is used outside the scope of a {@link cascading.flow.Flow} 190 * instance or if it participates in multiple times in a given Flow or across different Flows in 191 * a {@link cascading.cascade.Cascade}. 192 * <p/> 193 * Note this method will be called in context of this Tap being used as a traditional 'sink' and as a 'trap'. 194 * <p/> 195 * In the context of a Flow, it will be called after 196 * {@link cascading.flow.FlowListener#onStarting(cascading.flow.Flow)} 197 * <p/> 198 * Note that no resources or services should be modified by this method. If this Tap instance returns true for 199 * {@link #isReplace()}, then {@link #deleteResource(Object)} will be called by the parent Flow. 200 * 201 * @param flowProcess of type FlowProcess 202 * @param conf of type Config 203 */ 204 public void sinkConfInit( FlowProcess<Config> flowProcess, Config conf ) 205 { 206 getScheme().sinkConfInit( flowProcess, this, conf ); 207 } 208 209 /** 210 * Method getIdentifier returns a String representing the resource this Tap instance represents. 211 * <p/> 212 * Often, if the tap accesses a filesystem, the identifier is nothing more than the path to the file or directory. 213 * In other cases it may be a an URL or URI representing a connection string or remote resource. 214 * <p/> 215 * Any two Tap instances having the same value for the identifier are considered equal. 216 * 217 * @return String 218 */ 219 @Property(name = "identifier", visibility = Visibility.PUBLIC) 220 @PropertyDescription("The resource this Tap instance represents") 221 @PropertySanitizer("cascading.management.annotation.URISanitizer") 222 public abstract String getIdentifier(); 223 224 /** 225 * Method getSourceFields returns the sourceFields of this Tap object. 226 * 227 * @return the sourceFields (type Fields) of this Tap object. 228 */ 229 public Fields getSourceFields() 230 { 231 return getScheme().getSourceFields(); 232 } 233 234 /** 235 * Method getSinkFields returns the sinkFields of this Tap object. 236 * 237 * @return the sinkFields (type Fields) of this Tap object. 238 */ 239 public Fields getSinkFields() 240 { 241 return getScheme().getSinkFields(); 242 } 243 244 /** 245 * Method openForRead opens the resource represented by this Tap instance for reading. 246 * <p/> 247 * {@code input} value may be null, if so, sub-classes must inquire with the underlying {@link Scheme} 248 * via {@link Scheme#sourceConfInit(cascading.flow.FlowProcess, Tap, Object)} to get the proper 249 * input type and instantiate it before calling {@code super.openForRead()}. 250 * <p/> 251 * Note the returned iterator will return the same instance of {@link cascading.tuple.TupleEntry} on every call, 252 * thus a copy must be made of either the TupleEntry or the underlying {@code Tuple} instance if they are to be 253 * stored in a Collection. 254 * 255 * @param flowProcess of type FlowProcess 256 * @param input of type Input 257 * @return TupleEntryIterator 258 * @throws java.io.IOException when the resource cannot be opened 259 */ 260 public abstract TupleEntryIterator openForRead( FlowProcess<Config> flowProcess, Input input ) throws IOException; 261 262 /** 263 * Method openForRead opens the resource represented by this Tap instance for reading. 264 * <p/> 265 * Note the returned iterator will return the same instance of {@link cascading.tuple.TupleEntry} on every call, 266 * thus a copy must be made of either the TupleEntry or the underlying {@code Tuple} instance if they are to be 267 * stored in a Collection. 268 * 269 * @param flowProcess of type FlowProcess 270 * @return TupleEntryIterator 271 * @throws java.io.IOException when the resource cannot be opened 272 */ 273 public TupleEntryIterator openForRead( FlowProcess<Config> flowProcess ) throws IOException 274 { 275 return openForRead( flowProcess, null ); 276 } 277 278 /** 279 * Method openForWrite opens the resource represented by this Tap instance for writing. 280 * <p/> 281 * This method is used internally and does not honor the {@link SinkMode} setting. If SinkMode is 282 * {@link SinkMode#REPLACE}, this call may fail. See {@link #openForWrite(cascading.flow.FlowProcess)}. 283 * <p/> 284 * {@code output} value may be null, if so, sub-classes must inquire with the underlying {@link Scheme} 285 * via {@link Scheme#sinkConfInit(cascading.flow.FlowProcess, Tap, Object)} to get the proper 286 * output type and instantiate it before calling {@code super.openForWrite()}. 287 * 288 * @param flowProcess of type FlowProcess 289 * @param output of type Output 290 * @return TupleEntryCollector 291 * @throws java.io.IOException when the resource cannot be opened 292 */ 293 public abstract TupleEntryCollector openForWrite( FlowProcess<Config> flowProcess, Output output ) throws IOException; 294 295 /** 296 * Method openForWrite opens the resource represented by this Tap instance for writing. 297 * <p/> 298 * This method is for user application use and does honor the {@link SinkMode#REPLACE} settings. That is, if 299 * SinkMode is set to {@link SinkMode#REPLACE} the underlying resource will be deleted. 300 * <p/> 301 * Note if {@link SinkMode#UPDATE} is set, the resource will not be deleted. 302 * 303 * @param flowProcess of type FlowProcess 304 * @return TupleEntryCollector 305 * @throws java.io.IOException when the resource cannot be opened 306 */ 307 public TupleEntryCollector openForWrite( FlowProcess<Config> flowProcess ) throws IOException 308 { 309 if( isReplace() ) 310 deleteResource( flowProcess.getConfigCopy() ); 311 312 return openForWrite( flowProcess, null ); 313 } 314 315 @Override 316 public Scope outgoingScopeFor( Set<Scope> incomingScopes ) 317 { 318 // as a source Tap, we emit the scheme defined Fields 319 // as a sink Tap, we declare we emit the incoming Fields 320 // as a temp Tap, this method never gets called, but we emit what we consume 321 int count = 0; 322 for( Scope incomingScope : incomingScopes ) 323 { 324 Fields incomingFields = incomingScope.getIncomingTapFields(); 325 326 if( incomingFields != null ) 327 { 328 try 329 { 330 incomingFields.select( getSinkFields() ); 331 } 332 catch( FieldsResolverException exception ) 333 { 334 throw new TapException( this, exception.getSourceFields(), exception.getSelectorFields(), exception ); 335 } 336 337 count++; 338 } 339 } 340 341 if( count > 1 ) 342 throw new FlowException( "Tap may not have more than one incoming Scope" ); 343 344 // this allows the incoming to be passed through to the outgoing 345 Fields incomingFields = incomingScopes.size() == 0 ? null : incomingScopes.iterator().next().getIncomingTapFields(); 346 347 if( incomingFields != null && 348 ( isSource() && getSourceFields().equals( Fields.UNKNOWN ) || 349 isSink() && getSinkFields().equals( Fields.ALL ) ) ) 350 return new Scope( incomingFields ); 351 352 if( count == 1 ) 353 return new Scope( getSinkFields() ); 354 355 return new Scope( getSourceFields() ); 356 } 357 358 /** 359 * A hook for allowing a Scheme to lazily retrieve its source fields. 360 * 361 * @param flowProcess of type FlowProcess 362 * @return the found Fields 363 */ 364 public Fields retrieveSourceFields( FlowProcess<Config> flowProcess ) 365 { 366 return getScheme().retrieveSourceFields( flowProcess, this ); 367 } 368 369 public void presentSourceFields( FlowProcess<Config> flowProcess, Fields fields ) 370 { 371 getScheme().presentSourceFields( flowProcess, this, fields ); 372 } 373 374 /** 375 * A hook for allowing a Scheme to lazily retrieve its sink fields. 376 * 377 * @param flowProcess of type FlowProcess 378 * @return the found Fields 379 */ 380 public Fields retrieveSinkFields( FlowProcess<Config> flowProcess ) 381 { 382 return getScheme().retrieveSinkFields( flowProcess, this ); 383 } 384 385 public void presentSinkFields( FlowProcess<Config> flowProcess, Fields fields ) 386 { 387 getScheme().presentSinkFields( flowProcess, this, fields ); 388 } 389 390 @Override 391 public Fields resolveIncomingOperationArgumentFields( Scope incomingScope ) 392 { 393 return incomingScope.getIncomingTapFields(); 394 } 395 396 @Override 397 public Fields resolveIncomingOperationPassThroughFields( Scope incomingScope ) 398 { 399 return incomingScope.getIncomingTapFields(); 400 } 401 402 /** 403 * Method getFullIdentifier returns a fully qualified resource identifier. 404 * 405 * @param flowProcess of type FlowProcess 406 * @return String 407 */ 408 public String getFullIdentifier( FlowProcess<Config> flowProcess ) 409 { 410 return getFullIdentifier( flowProcess.getConfigCopy() ); 411 } 412 413 /** 414 * Method getFullIdentifier returns a fully qualified resource identifier. 415 * 416 * @param conf of type Config 417 * @return String 418 */ 419 public String getFullIdentifier( Config conf ) 420 { 421 return getIdentifier(); 422 } 423 424 /** 425 * Method createResource creates the underlying resource. 426 * 427 * @param flowProcess of type FlowProcess 428 * @return boolean 429 * @throws IOException when there is an error making directories 430 */ 431 public boolean createResource( FlowProcess<Config> flowProcess ) throws IOException 432 { 433 return createResource( flowProcess.getConfigCopy() ); 434 } 435 436 /** 437 * Method createResource creates the underlying resource. 438 * 439 * @param conf of type Config 440 * @return boolean 441 * @throws IOException when there is an error making directories 442 */ 443 public abstract boolean createResource( Config conf ) throws IOException; 444 445 /** 446 * Method deleteResource deletes the resource represented by this instance. 447 * 448 * @param flowProcess of type FlowProcess 449 * @return boolean 450 * @throws IOException when the resource cannot be deleted 451 */ 452 public boolean deleteResource( FlowProcess<Config> flowProcess ) throws IOException 453 { 454 return deleteResource( flowProcess.getConfigCopy() ); 455 } 456 457 /** 458 * Method deleteResource deletes the resource represented by this instance. 459 * 460 * @param conf of type Config 461 * @return boolean 462 * @throws IOException when the resource cannot be deleted 463 */ 464 public abstract boolean deleteResource( Config conf ) throws IOException; 465 466 /** 467 * Method commitResource allows the underlying resource to be notified when all write processing is 468 * successful so that any additional cleanup or processing may be completed. 469 * <p/> 470 * See {@link #rollbackResource(Object)} to handle cleanup in the face of failures. 471 * <p/> 472 * This method is invoked once "client side" and not in the cluster, if any. 473 * <p/> 474 * If other sink Tap instance in a given Flow fail on commitResource after called on this instance, 475 * rollbackResource will not be called. 476 * <p/> 477 * <emphasis>This is an experimental API and subject to refinement!!</emphasis> 478 * 479 * @param conf of type Config 480 * @return returns true if successful 481 * @throws IOException 482 */ 483 public boolean commitResource( Config conf ) throws IOException 484 { 485 return true; 486 } 487 488 /** 489 * Method rollbackResource allows the underlying resource to be notified when any write processing has failed or 490 * was stopped so that any cleanup may be started. 491 * <p/> 492 * See {@link #commitResource(Object)} to handle cleanup when the write has successfully completed. 493 * <p/> 494 * This method is invoked once "client side" and not in the cluster, if any. 495 * <p/> 496 * <emphasis>This is an experimental API and subject to refinement!!</emphasis> 497 * 498 * @param conf of type Config 499 * @return returns true if successful 500 * @throws IOException 501 */ 502 public boolean rollbackResource( Config conf ) throws IOException 503 { 504 return true; 505 } 506 507 /** 508 * Method resourceExists returns true if the path represented by this instance exists. 509 * 510 * @param flowProcess of type FlowProcess 511 * @return true if the underlying resource already exists 512 * @throws IOException when the status cannot be determined 513 */ 514 public boolean resourceExists( FlowProcess<Config> flowProcess ) throws IOException 515 { 516 return resourceExists( flowProcess.getConfigCopy() ); 517 } 518 519 /** 520 * Method resourceExists returns true if the path represented by this instance exists. 521 * 522 * @param conf of type Config 523 * @return true if the underlying resource already exists 524 * @throws IOException when the status cannot be determined 525 */ 526 public abstract boolean resourceExists( Config conf ) throws IOException; 527 528 /** 529 * Method getModifiedTime returns the date this resource was last modified. 530 * 531 * @param flowProcess of type FlowProcess 532 * @return The date this resource was last modified. 533 * @throws IOException 534 */ 535 public long getModifiedTime( FlowProcess<Config> flowProcess ) throws IOException 536 { 537 return getModifiedTime( flowProcess.getConfigCopy() ); 538 } 539 540 /** 541 * Method getModifiedTime returns the date this resource was last modified. 542 * 543 * @param conf of type Config 544 * @return The date this resource was last modified. 545 * @throws IOException 546 */ 547 public abstract long getModifiedTime( Config conf ) throws IOException; 548 549 /** 550 * Method getSinkMode returns the {@link SinkMode} }of this Tap object. 551 * 552 * @return the sinkMode (type SinkMode) of this Tap object. 553 */ 554 public SinkMode getSinkMode() 555 { 556 return sinkMode; 557 } 558 559 /** 560 * Method isKeep indicates whether the resource represented by this instance should be kept if it 561 * already exists when the Flow is started. 562 * 563 * @return boolean 564 */ 565 public boolean isKeep() 566 { 567 return sinkMode == SinkMode.KEEP; 568 } 569 570 /** 571 * Method isReplace indicates whether the resource represented by this instance should be deleted if it 572 * already exists when the Flow is started. 573 * 574 * @return boolean 575 */ 576 public boolean isReplace() 577 { 578 return sinkMode == SinkMode.REPLACE; 579 } 580 581 /** 582 * Method isUpdate indicates whether the resource represented by this instance should be updated if it already 583 * exists. Otherwise a new resource will be created, via {@link #createResource(Object)}, when the Flow is started. 584 * 585 * @return boolean 586 */ 587 public boolean isUpdate() 588 { 589 return sinkMode == SinkMode.UPDATE; 590 } 591 592 /** 593 * Method isSink returns true if this Tap instance can be used as a sink. 594 * 595 * @return boolean 596 */ 597 public boolean isSink() 598 { 599 return getScheme().isSink(); 600 } 601 602 /** 603 * Method isSource returns true if this Tap instance can be used as a source. 604 * 605 * @return boolean 606 */ 607 public boolean isSource() 608 { 609 return getScheme().isSource(); 610 } 611 612 /** 613 * Method isTemporary returns true if this Tap is temporary (used for intermediate results). 614 * 615 * @return the temporary (type boolean) of this Tap object. 616 */ 617 public boolean isTemporary() 618 { 619 return false; 620 } 621 622 /** 623 * Returns a {@link cascading.property.ConfigDef} instance that allows for local properties to be set and made available via 624 * a resulting {@link cascading.flow.FlowProcess} instance when the tap is invoked. 625 * <p/> 626 * Any properties set on the configDef will not show up in any {@link Flow} or {@link cascading.flow.FlowStep} process 627 * level configuration, but will override any of those values as seen by the current Tap instance method call where a 628 * FlowProcess is provided except for the {@link #sourceConfInit(cascading.flow.FlowProcess, Object)} and 629 * {@link #sinkConfInit(cascading.flow.FlowProcess, Object)} methods. 630 * <p/> 631 * That is, the {@code *confInit} methods are called before any ConfigDef is applied, so any values placed into 632 * a ConfigDef instance will not be visible to them. 633 * 634 * @return an instance of ConfigDef 635 */ 636 public ConfigDef getConfigDef() 637 { 638 if( configDef == null ) 639 configDef = new ConfigDef(); 640 641 return configDef; 642 } 643 644 /** 645 * Returns {@code true} if there are properties in the configDef instance. 646 * 647 * @return true if there are configDef properties 648 */ 649 public boolean hasConfigDef() 650 { 651 return configDef != null && !configDef.isEmpty(); 652 } 653 654 /** 655 * Returns a {@link ConfigDef} instance that allows for process level properties to be set and made available via 656 * a resulting {@link cascading.flow.FlowProcess} instance when the tap is invoked. 657 * <p/> 658 * Any properties set on the stepConfigDef will not show up in any Flow configuration, but will show up in 659 * the current process {@link cascading.flow.FlowStep} (in Hadoop the MapReduce jobconf). Any value set in the 660 * stepConfigDef will be overridden by the tap local {@code #getConfigDef} instance. 661 * </p> 662 * Use this method to tweak properties in the process step this tap instance is planned into. 663 * <p/> 664 * Note the {@code *confInit} methods are called before any ConfigDef is applied, so any values placed into 665 * a ConfigDef instance will not be visible to them. 666 * 667 * @return an instance of ConfigDef 668 */ 669 @Override 670 public ConfigDef getStepConfigDef() 671 { 672 if( processConfigDef == null ) 673 processConfigDef = new ConfigDef(); 674 675 return processConfigDef; 676 } 677 678 /** 679 * Returns {@code true} if there are properties in the processConfigDef instance. 680 * 681 * @return true if there are processConfigDef properties 682 */ 683 @Override 684 public boolean hasStepConfigDef() 685 { 686 return processConfigDef != null && !processConfigDef.isEmpty(); 687 } 688 689 @Override 690 public boolean isEquivalentTo( FlowElement element ) 691 { 692 if( element == null ) 693 return false; 694 695 if( this == element ) 696 return true; 697 698 boolean compare = getClass() == element.getClass(); 699 700 if( !compare ) 701 return false; 702 703 return equals( element ); 704 } 705 706 @Override 707 public boolean equals( Object object ) 708 { 709 if( this == object ) 710 return true; 711 if( object == null || getClass() != object.getClass() ) 712 return false; 713 714 Tap tap = (Tap) object; 715 716 if( getIdentifier() != null ? !getIdentifier().equals( tap.getIdentifier() ) : tap.getIdentifier() != null ) 717 return false; 718 719 if( getScheme() != null ? !getScheme().equals( tap.getScheme() ) : tap.getScheme() != null ) 720 return false; 721 722 return true; 723 } 724 725 @Override 726 public int hashCode() 727 { 728 int result = getIdentifier() != null ? getIdentifier().hashCode() : 0; 729 730 result = 31 * result + ( getScheme() != null ? getScheme().hashCode() : 0 ); 731 732 return result; 733 } 734 735 @Override 736 public String toString() 737 { 738 if( getIdentifier() != null ) 739 return getClass().getSimpleName() + "[\"" + getScheme() + "\"]" + "[\"" + Util.sanitizeUrl( getIdentifier() ) + "\"]"; // sanitize 740 else 741 return getClass().getSimpleName() + "[\"" + getScheme() + "\"]" + "[not initialized]"; 742 } 743 }