001/*
002 * Copyright (c) 2007-2017 Xplenty, 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
021package cascading.pipe;
022
023import java.lang.reflect.Type;
024import java.util.ArrayList;
025import java.util.Comparator;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.Iterator;
029import java.util.LinkedHashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.Set;
033
034import cascading.flow.planner.DeclaresResults;
035import cascading.flow.planner.Scope;
036import cascading.pipe.joiner.BufferJoin;
037import cascading.pipe.joiner.InnerJoin;
038import cascading.pipe.joiner.Joiner;
039import cascading.tuple.Fields;
040import cascading.tuple.FieldsResolverException;
041import cascading.tuple.TupleException;
042import cascading.tuple.coerce.Coercions;
043import cascading.tuple.type.CoercibleType;
044import cascading.util.Util;
045
046import static java.util.Arrays.asList;
047
048/**
049 * The base class for {@link GroupBy}, {@link CoGroup}, {@link Merge}, and {@link HashJoin}. This class should not be used directly.
050 *
051 * @see GroupBy
052 * @see CoGroup
053 * @see Merge
054 * @see HashJoin
055 */
056public class Splice extends Pipe
057  {
058  enum Kind
059    {
060      GroupBy, CoGroup, Merge, Join
061    }
062
063  private Kind kind;
064  /** Field spliceName */
065  private String spliceName;
066  /** Field pipes */
067  private final List<Pipe> pipes = new ArrayList<Pipe>();
068  /** Field groupFieldsMap */
069  protected final Map<String, Fields> keyFieldsMap = new LinkedHashMap<String, Fields>(); // keep order
070  /** Field sortFieldsMap */
071  protected Map<String, Fields> sortFieldsMap = new LinkedHashMap<String, Fields>(); // keep order
072  /** Field reverseOrder */
073  private boolean reverseOrder = false;
074  /** Field declaredFields */
075  protected Fields declaredFields;
076  /** Field resultGroupFields */
077  protected Fields resultGroupFields;
078  /** Field repeat */
079  private int numSelfJoins = 0;
080  /** Field coGrouper */
081  private Joiner joiner;
082
083  /** Field pipePos */
084  private transient Map<String, Integer> pipePos;
085
086  /**
087   * Constructor Splice creates a new Splice instance.
088   *
089   * @param lhs            of type Pipe
090   * @param lhsGroupFields of type Fields
091   * @param rhs            of type Pipe
092   * @param rhsGroupFields of type Fields
093   * @param declaredFields of type Fields
094   */
095  protected Splice( Pipe lhs, Fields lhsGroupFields, Pipe rhs, Fields rhsGroupFields, Fields declaredFields )
096    {
097    this( lhs, lhsGroupFields, rhs, rhsGroupFields, declaredFields, null, null );
098    }
099
100  /**
101   * Constructor Splice creates a new Splice instance.
102   *
103   * @param lhs               of type Pipe
104   * @param lhsGroupFields    of type Fields
105   * @param rhs               of type Pipe
106   * @param rhsGroupFields    of type Fields
107   * @param declaredFields    of type Fields
108   * @param resultGroupFields of type Fields
109   */
110  protected Splice( Pipe lhs, Fields lhsGroupFields, Pipe rhs, Fields rhsGroupFields, Fields declaredFields, Fields resultGroupFields )
111    {
112    this( lhs, lhsGroupFields, rhs, rhsGroupFields, declaredFields, resultGroupFields, null );
113    }
114
115  /**
116   * Constructor Splice creates a new Splice instance.
117   *
118   * @param lhs            of type Pipe
119   * @param lhsGroupFields of type Fields
120   * @param rhs            of type Pipe
121   * @param rhsGroupFields of type Fields
122   * @param declaredFields of type Fields
123   * @param joiner         of type CoGrouper
124   */
125  protected Splice( Pipe lhs, Fields lhsGroupFields, Pipe rhs, Fields rhsGroupFields, Fields declaredFields, Joiner joiner )
126    {
127    this( Pipe.pipes( lhs, rhs ), Fields.fields( lhsGroupFields, rhsGroupFields ), declaredFields, joiner );
128    }
129
130  /**
131   * Constructor Splice creates a new Splice instance.
132   *
133   * @param lhs               of type Pipe
134   * @param lhsGroupFields    of type Fields
135   * @param rhs               of type Pipe
136   * @param rhsGroupFields    of type Fields
137   * @param declaredFields    of type Fields
138   * @param resultGroupFields of type Fields
139   * @param joiner            of type Joiner
140   */
141  protected Splice( Pipe lhs, Fields lhsGroupFields, Pipe rhs, Fields rhsGroupFields, Fields declaredFields, Fields resultGroupFields, Joiner joiner )
142    {
143    this( Pipe.pipes( lhs, rhs ), Fields.fields( lhsGroupFields, rhsGroupFields ), declaredFields, resultGroupFields, joiner );
144    }
145
146  /**
147   * Constructor Splice creates a new Splice instance.
148   *
149   * @param lhs            of type Pipe
150   * @param lhsGroupFields of type Fields
151   * @param rhs            of type Pipe
152   * @param rhsGroupFields of type Fields
153   * @param joiner         of type CoGrouper
154   */
155  protected Splice( Pipe lhs, Fields lhsGroupFields, Pipe rhs, Fields rhsGroupFields, Joiner joiner )
156    {
157    this( lhs, lhsGroupFields, rhs, rhsGroupFields, null, joiner );
158    }
159
160  /**
161   * Constructor Splice creates a new Splice instance.
162   *
163   * @param lhs            of type Pipe
164   * @param lhsGroupFields of type Fields
165   * @param rhs            of type Pipe
166   * @param rhsGroupFields of type Fields
167   */
168  protected Splice( Pipe lhs, Fields lhsGroupFields, Pipe rhs, Fields rhsGroupFields )
169    {
170    this( Pipe.pipes( lhs, rhs ), Fields.fields( lhsGroupFields, rhsGroupFields ) );
171    }
172
173  /**
174   * Constructor Splice creates a new Splice instance.
175   *
176   * @param pipes of type Pipe...
177   */
178  protected Splice( Pipe... pipes )
179    {
180    this( pipes, (Fields[]) null );
181    }
182
183  /**
184   * Constructor Splice creates a new Splice instance.
185   *
186   * @param pipes       of type Pipe[]
187   * @param groupFields of type Fields[]
188   */
189  protected Splice( Pipe[] pipes, Fields[] groupFields )
190    {
191    this( null, pipes, groupFields, null, null );
192    }
193
194  /**
195   * Constructor Splice creates a new Splice instance.
196   *
197   * @param spliceName  of type String
198   * @param pipes       of type Pipe[]
199   * @param groupFields of type Fields[]
200   */
201  protected Splice( String spliceName, Pipe[] pipes, Fields[] groupFields )
202    {
203    this( spliceName, pipes, groupFields, null, null );
204    }
205
206  /**
207   * Constructor Splice creates a new Splice instance.
208   *
209   * @param spliceName     of type String
210   * @param pipes          of type Pipe[]
211   * @param groupFields    of type Fields[]
212   * @param declaredFields of type Fields
213   */
214  protected Splice( String spliceName, Pipe[] pipes, Fields[] groupFields, Fields declaredFields )
215    {
216    this( spliceName, pipes, groupFields, declaredFields, null );
217    }
218
219  /**
220   * Constructor Splice creates a new Splice instance.
221   *
222   * @param spliceName        of type String
223   * @param pipes             of type Pipe[]
224   * @param groupFields       of type Fields[]
225   * @param declaredFields    of type Fields
226   * @param resultGroupFields of type Fields
227   */
228  protected Splice( String spliceName, Pipe[] pipes, Fields[] groupFields, Fields declaredFields, Fields resultGroupFields )
229    {
230    this( spliceName, pipes, groupFields, declaredFields, resultGroupFields, null );
231    }
232
233  /**
234   * Constructor Splice creates a new Splice instance.
235   *
236   * @param pipes          of type Pipe[]
237   * @param groupFields    of type Fields[]
238   * @param declaredFields of type Fields
239   * @param joiner         of type CoGrouper
240   */
241  protected Splice( Pipe[] pipes, Fields[] groupFields, Fields declaredFields, Joiner joiner )
242    {
243    this( null, pipes, groupFields, declaredFields, null, joiner );
244    }
245
246  /**
247   * Constructor Splice creates a new Splice instance.
248   *
249   * @param pipes             of type Pipe[]
250   * @param groupFields       of type Fields[]
251   * @param declaredFields    of type Fields
252   * @param resultGroupFields of type Fields
253   * @param joiner            of type Joiner
254   */
255  protected Splice( Pipe[] pipes, Fields[] groupFields, Fields declaredFields, Fields resultGroupFields, Joiner joiner )
256    {
257    this( null, pipes, groupFields, declaredFields, resultGroupFields, joiner );
258    }
259
260  /**
261   * Constructor Splice creates a new Splice instance.
262   *
263   * @param spliceName     of type String
264   * @param pipes          of type Pipe[]
265   * @param groupFields    of type Fields[]
266   * @param declaredFields of type Fields
267   * @param joiner         of type CoGrouper
268   */
269  protected Splice( String spliceName, Pipe[] pipes, Fields[] groupFields, Fields declaredFields, Fields resultGroupFields, Joiner joiner )
270    {
271    if( pipes == null )
272      throw new IllegalArgumentException( "pipes array may not be null" );
273
274    setKind();
275    this.spliceName = spliceName;
276
277    int uniques = new HashSet<Pipe>( asList( Pipe.resolvePreviousAll( pipes ) ) ).size();
278
279    if( pipes.length > 1 && uniques == 1 )
280      {
281      if( isMerge() )
282        throw new IllegalArgumentException( "may not merge a pipe with itself without intermediate operations after the split" );
283
284      if( groupFields == null )
285        throw new IllegalArgumentException( "groupFields array may not be null" );
286
287      if( new HashSet<Fields>( asList( groupFields ) ).size() != 1 )
288        throw new IllegalArgumentException( "all groupFields must be identical" );
289
290      addPipe( pipes[ 0 ] );
291      this.numSelfJoins = pipes.length - 1;
292      this.keyFieldsMap.put( pipes[ 0 ].getName(), groupFields[ 0 ] );
293
294      if( resultGroupFields != null && groupFields[ 0 ].size() * pipes.length != resultGroupFields.size() )
295        throw new IllegalArgumentException( "resultGroupFields and cogroup joined fields must be same size" );
296      }
297    else
298      {
299      int last = -1;
300      for( int i = 0; i < pipes.length; i++ )
301        {
302        addPipe( pipes[ i ] );
303
304        if( groupFields == null || groupFields.length == 0 )
305          {
306          addGroupFields( pipes[ i ], Fields.FIRST );
307          continue;
308          }
309
310        if( last != -1 && last != groupFields[ i ].size() )
311          throw new IllegalArgumentException( "all groupFields must be same size" );
312
313        last = groupFields[ i ].size();
314        addGroupFields( pipes[ i ], groupFields[ i ] );
315        }
316
317      if( resultGroupFields != null && last * pipes.length != resultGroupFields.size() )
318        throw new IllegalArgumentException( "resultGroupFields and cogroup resulting joined fields must be same size" );
319      }
320
321    this.declaredFields = declaredFields;
322    this.resultGroupFields = resultGroupFields;
323    this.joiner = joiner;
324
325    verifyCoGrouper();
326    }
327
328  /**
329   * Constructor Splice creates a new Splice instance.
330   *
331   * @param spliceName     of type String
332   * @param lhs            of type Pipe
333   * @param lhsGroupFields of type Fields
334   * @param rhs            of type Pipe
335   * @param rhsGroupFields of type Fields
336   * @param declaredFields of type Fields
337   */
338  protected Splice( String spliceName, Pipe lhs, Fields lhsGroupFields, Pipe rhs, Fields rhsGroupFields, Fields declaredFields )
339    {
340    this( lhs, lhsGroupFields, rhs, rhsGroupFields, declaredFields );
341    this.spliceName = spliceName;
342    }
343
344  /**
345   * Constructor Splice creates a new Splice instance.
346   *
347   * @param spliceName        of type String
348   * @param lhs               of type Pipe
349   * @param lhsGroupFields    of type Fields
350   * @param rhs               of type Pipe
351   * @param rhsGroupFields    of type Fields
352   * @param declaredFields    of type Fields
353   * @param resultGroupFields of type Fields
354   */
355  protected Splice( String spliceName, Pipe lhs, Fields lhsGroupFields, Pipe rhs, Fields rhsGroupFields, Fields declaredFields, Fields resultGroupFields )
356    {
357    this( lhs, lhsGroupFields, rhs, rhsGroupFields, declaredFields, resultGroupFields );
358    this.spliceName = spliceName;
359    }
360
361  /**
362   * Constructor Splice creates a new Splice instance.
363   *
364   * @param spliceName     of type String
365   * @param lhs            of type Pipe
366   * @param lhsGroupFields of type Fields
367   * @param rhs            of type Pipe
368   * @param rhsGroupFields of type Fields
369   * @param declaredFields of type Fields
370   * @param joiner         of type CoGrouper
371   */
372  protected Splice( String spliceName, Pipe lhs, Fields lhsGroupFields, Pipe rhs, Fields rhsGroupFields, Fields declaredFields, Joiner joiner )
373    {
374    this( lhs, lhsGroupFields, rhs, rhsGroupFields, declaredFields, joiner );
375    this.spliceName = spliceName;
376    }
377
378  /**
379   * Constructor Splice creates a new Splice instance.
380   *
381   * @param spliceName        of type String
382   * @param lhs               of type Pipe
383   * @param lhsGroupFields    of type Fields
384   * @param rhs               of type Pipe
385   * @param rhsGroupFields    of type Fields
386   * @param declaredFields    of type Fields
387   * @param resultGroupFields of type Fields
388   * @param joiner            of type Joiner
389   */
390  protected Splice( String spliceName, Pipe lhs, Fields lhsGroupFields, Pipe rhs, Fields rhsGroupFields, Fields declaredFields, Fields resultGroupFields, Joiner joiner )
391    {
392    this( lhs, lhsGroupFields, rhs, rhsGroupFields, declaredFields, resultGroupFields, joiner );
393    this.spliceName = spliceName;
394    }
395
396  /**
397   * Constructor Splice creates a new Splice instance.
398   *
399   * @param spliceName     of type String
400   * @param lhs            of type Pipe
401   * @param lhsGroupFields of type Fields
402   * @param rhs            of type Pipe
403   * @param rhsGroupFields of type Fields
404   * @param joiner         of type CoGrouper
405   */
406  protected Splice( String spliceName, Pipe lhs, Fields lhsGroupFields, Pipe rhs, Fields rhsGroupFields, Joiner joiner )
407    {
408    this( lhs, lhsGroupFields, rhs, rhsGroupFields, joiner );
409    this.spliceName = spliceName;
410    }
411
412  /**
413   * Constructor Splice creates a new Splice instance.
414   *
415   * @param spliceName     of type String
416   * @param lhs            of type Pipe
417   * @param lhsGroupFields of type Fields
418   * @param rhs            of type Pipe
419   * @param rhsGroupFields of type Fields
420   */
421  protected Splice( String spliceName, Pipe lhs, Fields lhsGroupFields, Pipe rhs, Fields rhsGroupFields )
422    {
423    this( lhs, lhsGroupFields, rhs, rhsGroupFields );
424    this.spliceName = spliceName;
425    }
426
427  /**
428   * Constructor Splice creates a new Splice instance.
429   *
430   * @param spliceName of type String
431   * @param pipes      of type Pipe...
432   */
433  protected Splice( String spliceName, Pipe... pipes )
434    {
435    this( pipes );
436    this.spliceName = spliceName;
437    }
438
439  /**
440   * Constructor Splice creates a new Splice instance.
441   *
442   * @param pipe           of type Pipe
443   * @param groupFields    of type Fields
444   * @param numSelfJoins   of type int
445   * @param declaredFields of type Fields
446   */
447  protected Splice( Pipe pipe, Fields groupFields, int numSelfJoins, Fields declaredFields )
448    {
449    this( pipe, groupFields, numSelfJoins );
450    this.declaredFields = declaredFields;
451    }
452
453  /**
454   * Constructor Splice creates a new Splice instance.
455   *
456   * @param pipe              of type Pipe
457   * @param groupFields       of type Fields
458   * @param numSelfJoins      of type int
459   * @param declaredFields    of type Fields
460   * @param resultGroupFields of type Fields
461   */
462  protected Splice( Pipe pipe, Fields groupFields, int numSelfJoins, Fields declaredFields, Fields resultGroupFields )
463    {
464    this( pipe, groupFields, numSelfJoins );
465    this.declaredFields = declaredFields;
466    this.resultGroupFields = resultGroupFields;
467
468    if( resultGroupFields != null && groupFields.size() * numSelfJoins != resultGroupFields.size() )
469      throw new IllegalArgumentException( "resultGroupFields and cogroup resulting join fields must be same size" );
470    }
471
472  /**
473   * Constructor Splice creates a new Splice instance.
474   *
475   * @param pipe           of type Pipe
476   * @param groupFields    of type Fields
477   * @param numSelfJoins   of type int
478   * @param declaredFields of type Fields
479   * @param joiner         of type CoGrouper
480   */
481  protected Splice( Pipe pipe, Fields groupFields, int numSelfJoins, Fields declaredFields, Joiner joiner )
482    {
483    this( pipe, groupFields, numSelfJoins, declaredFields );
484    this.joiner = joiner;
485
486    verifyCoGrouper();
487    }
488
489  /**
490   * Constructor Splice creates a new Splice instance.
491   *
492   * @param pipe              of type Pipe
493   * @param groupFields       of type Fields
494   * @param numSelfJoins      of type int
495   * @param declaredFields    of type Fields
496   * @param resultGroupFields of type Fields
497   * @param joiner            of type Joiner
498   */
499  protected Splice( Pipe pipe, Fields groupFields, int numSelfJoins, Fields declaredFields, Fields resultGroupFields, Joiner joiner )
500    {
501    this( pipe, groupFields, numSelfJoins, declaredFields, resultGroupFields );
502    this.joiner = joiner;
503
504    verifyCoGrouper();
505    }
506
507  /**
508   * Constructor Splice creates a new Splice instance.
509   *
510   * @param pipe         of type Pipe
511   * @param groupFields  of type Fields
512   * @param numSelfJoins of type int
513   * @param joiner       of type CoGrouper
514   */
515  protected Splice( Pipe pipe, Fields groupFields, int numSelfJoins, Joiner joiner )
516    {
517    setKind();
518    addPipe( pipe );
519    this.keyFieldsMap.put( pipe.getName(), groupFields );
520    this.numSelfJoins = numSelfJoins;
521    this.joiner = joiner;
522
523    verifyCoGrouper();
524    }
525
526  /**
527   * Constructor Splice creates a new Splice instance.
528   *
529   * @param pipe         of type Pipe
530   * @param groupFields  of type Fields
531   * @param numSelfJoins of type int
532   */
533  protected Splice( Pipe pipe, Fields groupFields, int numSelfJoins )
534    {
535    this( pipe, groupFields, numSelfJoins, (Joiner) null );
536    }
537
538  /**
539   * Constructor Splice creates a new Splice instance.
540   *
541   * @param spliceName     of type String
542   * @param pipe           of type Pipe
543   * @param groupFields    of type Fields
544   * @param numSelfJoins   of type int
545   * @param declaredFields of type Fields
546   */
547  protected Splice( String spliceName, Pipe pipe, Fields groupFields, int numSelfJoins, Fields declaredFields )
548    {
549    this( pipe, groupFields, numSelfJoins, declaredFields );
550    this.spliceName = spliceName;
551    }
552
553  /**
554   * Constructor Splice creates a new Splice instance.
555   *
556   * @param spliceName        of type String
557   * @param pipe              of type Pipe
558   * @param groupFields       of type Fields
559   * @param numSelfJoins      of type int
560   * @param declaredFields    of type Fields
561   * @param resultGroupFields of type Fields
562   */
563  protected Splice( String spliceName, Pipe pipe, Fields groupFields, int numSelfJoins, Fields declaredFields, Fields resultGroupFields )
564    {
565    this( pipe, groupFields, numSelfJoins, declaredFields, resultGroupFields );
566    this.spliceName = spliceName;
567    }
568
569  /**
570   * Constructor Splice creates a new Splice instance.
571   *
572   * @param spliceName     of type String
573   * @param pipe           of type Pipe
574   * @param groupFields    of type Fields
575   * @param numSelfJoins   of type int
576   * @param declaredFields of type Fields
577   * @param joiner         of type CoGrouper
578   */
579  protected Splice( String spliceName, Pipe pipe, Fields groupFields, int numSelfJoins, Fields declaredFields, Joiner joiner )
580    {
581    this( pipe, groupFields, numSelfJoins, declaredFields, joiner );
582    this.spliceName = spliceName;
583    }
584
585  /**
586   * Constructor Splice creates a new Splice instance.
587   *
588   * @param spliceName        of type String
589   * @param pipe              of type Pipe
590   * @param groupFields       of type Fields
591   * @param numSelfJoins      of type int
592   * @param declaredFields    of type Fields
593   * @param resultGroupFields of type Fields
594   * @param joiner            of type Joiner
595   */
596  protected Splice( String spliceName, Pipe pipe, Fields groupFields, int numSelfJoins, Fields declaredFields, Fields resultGroupFields, Joiner joiner )
597    {
598    this( pipe, groupFields, numSelfJoins, declaredFields, resultGroupFields, joiner );
599    this.spliceName = spliceName;
600    }
601
602  /**
603   * Constructor Splice creates a new Splice instance.
604   *
605   * @param spliceName   of type String
606   * @param pipe         of type Pipe
607   * @param groupFields  of type Fields
608   * @param numSelfJoins of type int
609   * @param joiner       of type CoGrouper
610   */
611  protected Splice( String spliceName, Pipe pipe, Fields groupFields, int numSelfJoins, Joiner joiner )
612    {
613    this( pipe, groupFields, numSelfJoins, joiner );
614    this.spliceName = spliceName;
615    }
616
617  /**
618   * Constructor Splice creates a new Splice instance.
619   *
620   * @param spliceName   of type String
621   * @param pipe         of type Pipe
622   * @param groupFields  of type Fields
623   * @param numSelfJoins of type int
624   */
625  protected Splice( String spliceName, Pipe pipe, Fields groupFields, int numSelfJoins )
626    {
627    this( pipe, groupFields, numSelfJoins );
628    this.spliceName = spliceName;
629    }
630
631  ////////////
632  // GROUPBY
633  ////////////
634
635  /**
636   * Constructor Splice creates a new Splice instance where grouping occurs on {@link Fields#ALL} fields.
637   *
638   * @param pipe of type Pipe
639   */
640  protected Splice( Pipe pipe )
641    {
642    this( null, pipe, Fields.ALL, null, false );
643    }
644
645  /**
646   * Constructor Splice creates a new Splice instance.
647   *
648   * @param pipe        of type Pipe
649   * @param groupFields of type Fields
650   */
651  protected Splice( Pipe pipe, Fields groupFields )
652    {
653    this( null, pipe, groupFields, null, false );
654    }
655
656  /**
657   * Constructor Splice creates a new Splice instance.
658   *
659   * @param spliceName  of type String
660   * @param pipe        of type Pipe
661   * @param groupFields of type Fields
662   */
663  protected Splice( String spliceName, Pipe pipe, Fields groupFields )
664    {
665    this( spliceName, pipe, groupFields, null, false );
666    }
667
668  /**
669   * Constructor Splice creates a new Splice instance.
670   *
671   * @param pipe        of type Pipe
672   * @param groupFields of type Fields
673   * @param sortFields  of type Fields
674   */
675  protected Splice( Pipe pipe, Fields groupFields, Fields sortFields )
676    {
677    this( null, pipe, groupFields, sortFields, false );
678    }
679
680  /**
681   * Constructor Splice creates a new Splice instance.
682   *
683   * @param spliceName  of type String
684   * @param pipe        of type Pipe
685   * @param groupFields of type Fields
686   * @param sortFields  of type Fields
687   */
688  protected Splice( String spliceName, Pipe pipe, Fields groupFields, Fields sortFields )
689    {
690    this( spliceName, pipe, groupFields, sortFields, false );
691    }
692
693  /**
694   * Constructor Splice creates a new Splice instance.
695   *
696   * @param pipe         of type Pipe
697   * @param groupFields  of type Fields
698   * @param sortFields   of type Fields
699   * @param reverseOrder of type boolean
700   */
701  protected Splice( Pipe pipe, Fields groupFields, Fields sortFields, boolean reverseOrder )
702    {
703    this( null, pipe, groupFields, sortFields, reverseOrder );
704    }
705
706  /**
707   * Constructor Splice creates a new Splice instance.
708   *
709   * @param spliceName   of type String
710   * @param pipe         of type Pipe
711   * @param groupFields  of type Fields
712   * @param sortFields   of type Fields
713   * @param reverseOrder of type boolean
714   */
715  protected Splice( String spliceName, Pipe pipe, Fields groupFields, Fields sortFields, boolean reverseOrder )
716    {
717    this( spliceName, Pipe.pipes( pipe ), groupFields, sortFields, reverseOrder );
718    }
719
720  /**
721   * Constructor Splice creates a new Splice instance.
722   *
723   * @param pipes       of type Pipe
724   * @param groupFields of type Fields
725   */
726  protected Splice( Pipe[] pipes, Fields groupFields )
727    {
728    this( null, pipes, groupFields, null, false );
729    }
730
731  /**
732   * Constructor Splice creates a new Splice instance.
733   *
734   * @param spliceName  of type String
735   * @param pipes       of type Pipe
736   * @param groupFields of type Fields
737   */
738  protected Splice( String spliceName, Pipe[] pipes, Fields groupFields )
739    {
740    this( spliceName, pipes, groupFields, null, false );
741    }
742
743  /**
744   * Constructor Splice creates a new Splice instance.
745   *
746   * @param pipes       of type Pipe
747   * @param groupFields of type Fields
748   * @param sortFields  of type Fields
749   */
750  protected Splice( Pipe[] pipes, Fields groupFields, Fields sortFields )
751    {
752    this( null, pipes, groupFields, sortFields, false );
753    }
754
755  /**
756   * Constructor Splice creates a new Splice instance.
757   *
758   * @param spliceName  of type String
759   * @param pipe        of type Pipe
760   * @param groupFields of type Fields
761   * @param sortFields  of type Fields
762   */
763  protected Splice( String spliceName, Pipe[] pipe, Fields groupFields, Fields sortFields )
764    {
765    this( spliceName, pipe, groupFields, sortFields, false );
766    }
767
768  /**
769   * Constructor Splice creates a new Splice instance.
770   *
771   * @param pipes        of type Pipe
772   * @param groupFields  of type Fields
773   * @param sortFields   of type Fields
774   * @param reverseOrder of type boolean
775   */
776  protected Splice( Pipe[] pipes, Fields groupFields, Fields sortFields, boolean reverseOrder )
777    {
778    this( null, pipes, groupFields, sortFields, reverseOrder );
779    }
780
781  /**
782   * Constructor Splice creates a new Splice instance.
783   *
784   * @param spliceName   of type String
785   * @param pipes        of type Pipe[]
786   * @param groupFields  of type Fields
787   * @param sortFields   of type Fields
788   * @param reverseOrder of type boolean
789   */
790  protected Splice( String spliceName, Pipe[] pipes, Fields groupFields, Fields sortFields, boolean reverseOrder )
791    {
792    if( pipes == null )
793      throw new IllegalArgumentException( "pipes array may not be null" );
794
795    if( groupFields == null )
796      throw new IllegalArgumentException( "groupFields may not be null" );
797
798    setKind();
799    this.spliceName = spliceName;
800
801    for( Pipe pipe : pipes )
802      {
803      addPipe( pipe );
804      this.keyFieldsMap.put( pipe.getName(), groupFields );
805
806      if( sortFields != null )
807        this.sortFieldsMap.put( pipe.getName(), sortFields );
808      }
809
810    this.reverseOrder = reverseOrder;
811    this.joiner = new InnerJoin();
812    }
813
814  private void verifyCoGrouper()
815    {
816    if( isJoin() && joiner instanceof BufferJoin )
817      throw new IllegalArgumentException( "invalid joiner, may not use BufferJoiner in a HashJoin" );
818
819    if( joiner == null )
820      {
821      joiner = new InnerJoin();
822      return;
823      }
824
825    if( joiner.numJoins() == -1 )
826      return;
827
828    int joins = Math.max( numSelfJoins, keyFieldsMap.size() - 1 ); // joining two streams is one join
829
830    if( joins != joiner.numJoins() )
831      throw new IllegalArgumentException( "invalid joiner, only accepts " + joiner.numJoins() + " joins, there are: " + joins );
832    }
833
834  private void setKind()
835    {
836    if( this instanceof GroupBy )
837      kind = Kind.GroupBy;
838    else if( this instanceof CoGroup )
839      kind = Kind.CoGroup;
840    else if( this instanceof Merge )
841      kind = Kind.Merge;
842    else
843      kind = Kind.Join;
844    }
845
846  /**
847   * Method getDeclaredFields returns the declaredFields of this Splice object.
848   *
849   * @return the declaredFields (type Fields) of this Splice object.
850   */
851  public Fields getDeclaredFields()
852    {
853    return declaredFields;
854    }
855
856  private void addPipe( Pipe pipe )
857    {
858    if( pipe.getName() == null )
859      throw new IllegalArgumentException( "each input pipe must have a name" );
860
861    pipes.add( pipe ); // allow same pipe
862    }
863
864  private void addGroupFields( Pipe pipe, Fields fields )
865    {
866    if( keyFieldsMap.containsKey( pipe.getName() ) )
867      throw new IllegalArgumentException( "each input pipe branch must be uniquely named" );
868
869    keyFieldsMap.put( pipe.getName(), fields );
870    }
871
872  @Override
873  public String getName()
874    {
875    if( spliceName != null )
876      return spliceName;
877
878    StringBuffer buffer = new StringBuffer();
879
880    for( Pipe pipe : pipes )
881      {
882      if( buffer.length() != 0 )
883        {
884        if( isGroupBy() || isMerge() )
885          buffer.append( "+" );
886        else if( isCoGroup() || isJoin() )
887          buffer.append( "*" ); // more semantically correct
888        }
889
890      buffer.append( pipe.getName() );
891      }
892
893    spliceName = buffer.toString();
894
895    return spliceName;
896    }
897
898  @Override
899  public Pipe[] getPrevious()
900    {
901    return pipes.toArray( new Pipe[ pipes.size() ] );
902    }
903
904  /**
905   * Method getGroupingSelectors returns the groupingSelectors of this Splice object.
906   *
907   * @return the groupingSelectors (type Map<String, Fields>) of this Splice object.
908   */
909  public Map<String, Fields> getKeySelectors()
910    {
911    return keyFieldsMap;
912    }
913
914  /**
915   * Method getSortingSelectors returns the sortingSelectors of this Splice object.
916   *
917   * @return the sortingSelectors (type Map<String, Fields>) of this Splice object.
918   */
919  public Map<String, Fields> getSortingSelectors()
920    {
921    return sortFieldsMap;
922    }
923
924  /**
925   * Method isSorted returns true if this Splice instance is sorting values other than the group fields.
926   *
927   * @return the sorted (type boolean) of this Splice object.
928   */
929  public boolean isSorted()
930    {
931    return !sortFieldsMap.isEmpty();
932    }
933
934  /**
935   * Method isSortReversed returns true if sorting is reversed.
936   *
937   * @return the sortReversed (type boolean) of this Splice object.
938   */
939  public boolean isSortReversed()
940    {
941    return reverseOrder;
942    }
943
944  public synchronized Map<String, Integer> getPipePos()
945    {
946    if( pipePos != null )
947      return pipePos;
948
949    pipePos = new HashMap<String, Integer>();
950
951    int pos = 0;
952    for( Object pipe : pipes )
953      pipePos.put( ( (Pipe) pipe ).getName(), pos++ );
954
955    return pipePos;
956    }
957
958  public Joiner getJoiner()
959    {
960    return joiner;
961    }
962
963  /**
964   * Method isGroupBy returns true if this Splice instance will perform a GroupBy operation.
965   *
966   * @return the groupBy (type boolean) of this Splice object.
967   */
968  public final boolean isGroupBy()
969    {
970    return kind == Kind.GroupBy;
971    }
972
973  public final boolean isCoGroup()
974    {
975    return kind == Kind.CoGroup;
976    }
977
978  public final boolean isMerge()
979    {
980    return kind == Kind.Merge;
981    }
982
983  public final boolean isJoin()
984    {
985    return kind == Kind.Join;
986    }
987
988  public int getNumSelfJoins()
989    {
990    return numSelfJoins;
991    }
992
993  public boolean isSelfJoin()
994    {
995    return numSelfJoins != 0;
996    }
997
998  // FIELDS
999
1000  @Override
1001  public Scope outgoingScopeFor( Set<Scope> incomingScopes )
1002    {
1003    Map<String, Fields> groupingSelectors = resolveGroupingSelectors( incomingScopes );
1004    Map<String, Fields> sortingSelectors = resolveSortingSelectors( incomingScopes );
1005    Fields declared = resolveDeclared( incomingScopes );
1006
1007    Fields outGroupingFields = resultGroupFields;
1008
1009    if( outGroupingFields == null && isCoGroup() )
1010      outGroupingFields = createJoinFields( incomingScopes, groupingSelectors, declared );
1011
1012    // for Group, the outgoing fields are the same as those declared
1013    Scope.Kind kind = getScopeKind();
1014
1015    return new Scope( getName(), declared, outGroupingFields, groupingSelectors, sortingSelectors, declared, kind );
1016    }
1017
1018  private Scope.Kind getScopeKind()
1019    {
1020    switch( kind )
1021      {
1022      case GroupBy:
1023        return Scope.Kind.GROUPBY;
1024      case CoGroup:
1025        return Scope.Kind.COGROUP;
1026      case Merge:
1027        return Scope.Kind.MERGE;
1028      case Join:
1029        return Scope.Kind.HASHJOIN;
1030      }
1031
1032    throw new IllegalStateException( "unknown kind: " + kind );
1033    }
1034
1035  private Fields createJoinFields( Set<Scope> incomingScopes, Map<String, Fields> groupingSelectors, Fields declared )
1036    {
1037    if( declared.isNone() )
1038      declared = Fields.UNKNOWN;
1039
1040    Map<String, Fields> incomingFields = new HashMap<String, Fields>();
1041
1042    for( Scope scope : incomingScopes )
1043      incomingFields.put( scope.getName(), scope.getIncomingSpliceFields() );
1044
1045    Fields outGroupingFields = Fields.NONE;
1046
1047    int offset = 0;
1048    for( Pipe pipe : pipes ) // need to retain order of pipes
1049      {
1050      String pipeName = pipe.getName();
1051      Fields pipeGroupingSelector = groupingSelectors.get( pipeName );
1052      Fields incomingField = incomingFields.get( pipeName );
1053
1054      if( !pipeGroupingSelector.isNone() )
1055        {
1056        Fields offsetFields = incomingField.selectPos( pipeGroupingSelector, offset );
1057        Fields resolvedSelect = declared.select( offsetFields );
1058
1059        outGroupingFields = outGroupingFields.append( resolvedSelect );
1060        }
1061
1062      offset += incomingField.size();
1063      }
1064
1065    return outGroupingFields;
1066    }
1067
1068  Map<String, Fields> resolveGroupingSelectors( Set<Scope> incomingScopes )
1069    {
1070    try
1071      {
1072      Map<String, Fields> groupingSelectors = getKeySelectors();
1073      Map<String, Fields> groupingFields = resolveSelectorsAgainstIncoming( incomingScopes, groupingSelectors, "grouping" );
1074
1075      if( !verifySameSize( groupingFields ) )
1076        throw new OperatorException( this, "all grouping fields must be same size: " + toString() );
1077
1078      verifySameTypes( groupingSelectors, groupingFields );
1079
1080      return groupingFields;
1081      }
1082    catch( FieldsResolverException exception )
1083      {
1084      throw new OperatorException( this, OperatorException.Kind.grouping, exception.getSourceFields(), exception.getSelectorFields(), exception );
1085      }
1086    catch( RuntimeException exception )
1087      {
1088      throw new OperatorException( this, "could not resolve grouping selector in: " + this, exception );
1089      }
1090    }
1091
1092  private boolean verifySameTypes( Map<String, Fields> groupingSelectors, Map<String, Fields> groupingFields )
1093    {
1094    // create array of field positions with comparators from the grouping selectors
1095    // unsure which side has the comparators declared so make a union
1096    boolean[] hasComparator = new boolean[ groupingFields.values().iterator().next().size() ];
1097
1098    for( Map.Entry<String, Fields> entry : groupingSelectors.entrySet() )
1099      {
1100      Comparator[] comparatorsArray = entry.getValue().getComparators();
1101
1102      for( int i = 0; i < comparatorsArray.length; i++ )
1103        hasComparator[ i ] = hasComparator[ i ] || comparatorsArray[ i ] != null;
1104      }
1105
1106    // compare all the rhs fields with the lhs (lhs and rhs are arbitrary here)
1107    Iterator<Fields> iterator = groupingFields.values().iterator();
1108    Fields lhsFields = iterator.next();
1109    Type[] lhsTypes = lhsFields.getTypes();
1110
1111    // if types are null, no basis for comparison
1112    if( lhsTypes == null )
1113      return true;
1114
1115    while( iterator.hasNext() )
1116      {
1117      Fields rhsFields = iterator.next();
1118      Type[] rhsTypes = rhsFields.getTypes();
1119
1120      // if types are null, no basis for comparison
1121      if( rhsTypes == null )
1122        return true;
1123
1124      for( int i = 0; i < lhsTypes.length; i++ )
1125        {
1126        if( hasComparator[ i ] )
1127          continue;
1128
1129        Type lhs = lhsTypes[ i ];
1130        Type rhs = rhsTypes[ i ];
1131
1132        lhs = getCanonicalType( lhs );
1133        rhs = getCanonicalType( rhs );
1134
1135        if( lhs.equals( rhs ) )
1136          continue;
1137
1138        Fields lhsError = new Fields( lhsFields.get( i ), lhsFields.getType( i ) );
1139        Fields rhsError = new Fields( rhsFields.get( i ), rhsFields.getType( i ) );
1140
1141        throw new OperatorException( this, "grouping fields must declare same types:" + lhsError.printVerbose() + " not same as " + rhsError.printVerbose() );
1142        }
1143      }
1144
1145    return true;
1146    }
1147
1148  private Type getCanonicalType( Type type )
1149    {
1150    if( type instanceof CoercibleType )
1151      type = ( (CoercibleType) type ).getCanonicalType();
1152
1153    // if one side is primitive, normalize to its primitive wrapper type
1154    if( type instanceof Class )
1155      type = Coercions.asNonPrimitive( (Class) type );
1156
1157    return type;
1158    }
1159
1160  private boolean verifySameSize( Map<String, Fields> groupingFields )
1161    {
1162    Iterator<Fields> iterator = groupingFields.values().iterator();
1163    int size = iterator.next().size();
1164
1165    while( iterator.hasNext() )
1166      {
1167      Fields groupingField = iterator.next();
1168
1169      if( groupingField.size() != size )
1170        return false;
1171
1172      size = groupingField.size();
1173      }
1174
1175    return true;
1176    }
1177
1178  private Map<String, Fields> resolveSelectorsAgainstIncoming( Set<Scope> incomingScopes, Map<String, Fields> selectors, String type )
1179    {
1180    Map<String, Fields> resolvedFields = new HashMap<String, Fields>();
1181
1182    for( Scope incomingScope : incomingScopes )
1183      {
1184      Fields selector = selectors.get( incomingScope.getName() );
1185
1186      if( selector == null )
1187        throw new OperatorException( this, "no " + type + " selector found for: " + incomingScope.getName() );
1188
1189      Fields incomingFields;
1190
1191      if( selector.isNone() )
1192        incomingFields = Fields.NONE;
1193      else if( selector.isAll() )
1194        incomingFields = incomingScope.getIncomingSpliceFields();
1195      else if( selector.isGroup() )
1196        incomingFields = incomingScope.getOutGroupingFields();
1197      else if( selector.isValues() )
1198        incomingFields = incomingScope.getOutValuesFields().subtract( incomingScope.getOutGroupingFields() );
1199      else
1200        incomingFields = incomingScope.getIncomingSpliceFields().select( selector );
1201
1202      resolvedFields.put( incomingScope.getName(), incomingFields );
1203      }
1204
1205    return resolvedFields;
1206    }
1207
1208  Map<String, Fields> resolveSortingSelectors( Set<Scope> incomingScopes )
1209    {
1210    try
1211      {
1212      if( getSortingSelectors().isEmpty() )
1213        return null;
1214
1215      return resolveSelectorsAgainstIncoming( incomingScopes, getSortingSelectors(), "sorting" );
1216      }
1217    catch( FieldsResolverException exception )
1218      {
1219      throw new OperatorException( this, OperatorException.Kind.sorting, exception.getSourceFields(), exception.getSelectorFields(), exception );
1220      }
1221    catch( RuntimeException exception )
1222      {
1223      throw new OperatorException( this, "could not resolve sorting selector in: " + this, exception );
1224      }
1225    }
1226
1227  @Override
1228  public Fields resolveIncomingOperationPassThroughFields( Scope incomingScope )
1229    {
1230    return incomingScope.getIncomingSpliceFields();
1231    }
1232
1233  Fields resolveDeclared( Set<Scope> incomingScopes )
1234    {
1235    try
1236      {
1237      Fields declaredFields = getJoinDeclaredFields();
1238
1239      // Fields.NONE is a flag to the CoGroup the following Buffer will use the JoinerClosure directly
1240      if( declaredFields != null && declaredFields.isNone() )
1241        {
1242        if( !isCoGroup() )
1243          throw new IllegalArgumentException( "Fields.NONE may only be declared as the join fields when using a CoGroup" );
1244
1245        return Fields.NONE;
1246        }
1247
1248      if( declaredFields != null ) // null for GroupBy
1249        {
1250        if( incomingScopes.size() != pipes.size() && isSelfJoin() )
1251          throw new OperatorException( this, "self joins without intermediate operators are not permitted, see 'numSelfJoins' constructor or identity function" );
1252
1253        int size = 0;
1254        boolean foundUnknown = false;
1255
1256        List<Fields> appendableFields = getOrderedResolvedFields( incomingScopes );
1257
1258        for( Fields fields : appendableFields )
1259          {
1260          foundUnknown = foundUnknown || fields.isUnknown();
1261          size += fields.size();
1262          }
1263
1264        // we must relax field checking in the face of unknown fields
1265        if( !foundUnknown && declaredFields.size() != size * ( numSelfJoins + 1 ) )
1266          {
1267          if( isSelfJoin() )
1268            throw new OperatorException( this, "declared grouped fields not same size as grouped values, declared: " + declaredFields.printVerbose() + " != size: " + size * ( numSelfJoins + 1 ) );
1269          else
1270            throw new OperatorException( this, "declared grouped fields not same size as grouped values, declared: " + declaredFields.printVerbose() + " resolved: " + Util.print( appendableFields, "" ) );
1271          }
1272
1273        int i = 0;
1274        for( Fields appendableField : appendableFields )
1275          {
1276          Type[] types = appendableField.getTypes();
1277
1278          if( types == null )
1279            {
1280            i += appendableField.size();
1281            continue;
1282            }
1283
1284          for( Type type : types )
1285            {
1286            if( type != null )
1287              declaredFields = declaredFields.applyType( i, type );
1288
1289            i++;
1290            }
1291          }
1292
1293        return declaredFields;
1294        }
1295
1296      // support merge or cogrouping here
1297      if( isGroupBy() || isMerge() )
1298        {
1299        Iterator<Scope> iterator = incomingScopes.iterator();
1300        Fields commonFields = iterator.next().getIncomingSpliceFields();
1301
1302        while( iterator.hasNext() )
1303          {
1304          Scope incomingScope = iterator.next();
1305          Fields fields = incomingScope.getIncomingSpliceFields();
1306
1307          if( !commonFields.equalsFields( fields ) )
1308            throw new OperatorException( this, "merged streams must declare the same field names, in the same order, expected: " + commonFields.printVerbose() + " found: " + fields.printVerbose() );
1309          }
1310
1311        return commonFields;
1312        }
1313      else
1314        {
1315        List<Fields> appendableFields = getOrderedResolvedFields( incomingScopes );
1316        Fields appendedFields = new Fields();
1317
1318        try
1319          {
1320          // will fail on name collisions
1321          for( Fields appendableField : appendableFields )
1322            appendedFields = appendedFields.append( appendableField );
1323          }
1324        catch( TupleException exception )
1325          {
1326          String fields = "";
1327
1328          for( Fields appendableField : appendableFields )
1329            fields += appendableField.print();
1330
1331          throw new OperatorException( this, "found duplicate field names in joined tuple stream: " + fields, exception );
1332          }
1333
1334        return appendedFields;
1335        }
1336      }
1337    catch( OperatorException exception )
1338      {
1339      throw exception;
1340      }
1341    catch( RuntimeException exception )
1342      {
1343      throw new OperatorException( this, "could not resolve declared fields in: " + this, exception );
1344      }
1345    }
1346
1347  public Fields getJoinDeclaredFields()
1348    {
1349    Fields declaredFields = getDeclaredFields();
1350
1351    if( !( joiner instanceof DeclaresResults ) )
1352      return declaredFields;
1353
1354    if( declaredFields == null && ( (DeclaresResults) joiner ).getFieldDeclaration() != null )
1355      declaredFields = ( (DeclaresResults) joiner ).getFieldDeclaration();
1356
1357    return declaredFields;
1358    }
1359
1360  private List<Fields> getOrderedResolvedFields( Set<Scope> incomingScopes )
1361    {
1362    Map<String, Scope> scopesMap = new HashMap<String, Scope>();
1363
1364    for( Scope incomingScope : incomingScopes )
1365      scopesMap.put( incomingScope.getName(), incomingScope );
1366
1367    List<Fields> appendableFields = new ArrayList<Fields>();
1368
1369    for( Pipe pipe : pipes )
1370      appendableFields.add( scopesMap.get( pipe.getName() ).getIncomingSpliceFields() );
1371    return appendableFields;
1372    }
1373
1374  // OBJECT OVERRIDES
1375
1376  @Override
1377  @SuppressWarnings({"RedundantIfStatement"})
1378  public boolean equals( Object object )
1379    {
1380    if( this == object )
1381      return true;
1382    if( object == null || getClass() != object.getClass() )
1383      return false;
1384    if( !super.equals( object ) )
1385      return false;
1386
1387    Splice splice = (Splice) object;
1388
1389    if( spliceName != null ? !spliceName.equals( splice.spliceName ) : splice.spliceName != null )
1390      return false;
1391    if( keyFieldsMap != null ? !keyFieldsMap.equals( splice.keyFieldsMap ) : splice.keyFieldsMap != null )
1392      return false;
1393    if( pipes != null ? !pipes.equals( splice.pipes ) : splice.pipes != null )
1394      return false;
1395
1396    return true;
1397    }
1398
1399  @Override
1400  public int hashCode()
1401    {
1402    int result = super.hashCode();
1403    result = 31 * result + ( pipes != null ? pipes.hashCode() : 0 );
1404    result = 31 * result + ( keyFieldsMap != null ? keyFieldsMap.hashCode() : 0 );
1405    result = 31 * result + ( spliceName != null ? spliceName.hashCode() : 0 );
1406    return result;
1407    }
1408
1409  @Override
1410  public String toString()
1411    {
1412    StringBuilder buffer = new StringBuilder( super.toString() );
1413
1414    buffer.append( "[by:" );
1415
1416    for( String name : keyFieldsMap.keySet() )
1417      {
1418      if( keyFieldsMap.size() > 1 )
1419        buffer.append( " " ).append( name ).append( ":" );
1420
1421      buffer.append( keyFieldsMap.get( name ).printVerbose() );
1422      }
1423
1424    if( isSelfJoin() )
1425      buffer.append( "[numSelfJoins:" ).append( numSelfJoins ).append( "]" );
1426
1427    buffer.append( "]" );
1428
1429    return buffer.toString();
1430    }
1431
1432  @Override
1433  protected void printInternal( StringBuffer buffer, Scope scope )
1434    {
1435    super.printInternal( buffer, scope );
1436    Map<String, Fields> map = scope.getKeySelectors();
1437
1438    if( map != null )
1439      {
1440      buffer.append( "[by:" );
1441
1442      // important to retain incoming pipe order
1443      for( Map.Entry<String, Fields> entry : keyFieldsMap.entrySet() )
1444        {
1445        String name = entry.getKey();
1446
1447        if( map.size() > 1 )
1448          buffer.append( name ).append( ":" );
1449
1450        Fields keys = map.get( name );
1451
1452        // if keys null, this is likely an edge contracted map
1453        if( keys == null )
1454          buffer.append( "<unavailable>" );
1455        else
1456          buffer.append( keys.print() ); // get resolved keys
1457        }
1458
1459      if( isSelfJoin() )
1460        buffer.append( "[numSelfJoins:" ).append( numSelfJoins ).append( "]" );
1461
1462      buffer.append( "]" );
1463      }
1464    }
1465  }