    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      }