001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2017 MicroBean.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014 * implied.  See the License for the specific language governing
015 * permissions and limitations under the License.
016 */
017package org.microbean.helm.chart;
018
019import java.beans.IntrospectionException;
020import java.beans.PropertyDescriptor;
021import java.beans.SimpleBeanInfo;
022
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.HashMap;
026import java.util.List;
027import java.util.ListIterator;
028import java.util.Map;
029import java.util.Objects;
030
031import java.util.regex.Pattern;
032
033import com.github.zafarkhaja.semver.Parser;
034import com.github.zafarkhaja.semver.Version;
035
036import com.github.zafarkhaja.semver.expr.Expression;
037import com.github.zafarkhaja.semver.expr.ExpressionParser;
038
039import com.google.protobuf.Any;
040import com.google.protobuf.ByteString;
041
042import hapi.chart.ChartOuterClass.Chart;
043import hapi.chart.ChartOuterClass.ChartOrBuilder;
044import hapi.chart.ConfigOuterClass.Config;
045import hapi.chart.ConfigOuterClass.ConfigOrBuilder;
046import hapi.chart.MetadataOuterClass.Metadata;
047import hapi.chart.MetadataOuterClass.MetadataOrBuilder;
048
049import org.yaml.snakeyaml.Yaml;
050
051/**
052 * A specification of a <a
053 * href="https://docs.helm.sh/developing_charts/#chart-dependencies">Helm
054 * chart's dependencies</a>; not normally used directly by end users.
055 *
056 * <p>Helm charts support a {@code requirements.yaml} resource, in
057 * YAML format, whose sole member is a {@code dependencies} list.
058 * This class represents that resource.</p>
059 *
060 * <h2>Thread Safety</h2>
061 *
062 * <p>Instances of this class are <strong>not</strong> suitable for
063 * concurrent access by multiple threads.</p>
064 *
065 * @author <a href="https://about.me/lairdnelson/"
066 * target="_parent">Laird Nelson</a>
067 */
068public final class Requirements {
069
070
071  /*
072   * Instance fields.
073   */
074
075
076  /**
077   * The {@link Collection} of {@link Dependency} instances that
078   * comprises this {@link Requirements}.
079   *
080   * <p>This field may be {@code null}.</p>
081   */
082  private Collection<Dependency> dependencies;
083
084
085  /*
086   * Constructors.
087   */
088
089
090  /**
091   * Creates a new {@link Requirements}.
092   */
093  public Requirements() {
094    super();
095  }
096
097
098  /*
099   * Instance methods.
100   */
101  
102
103  /**
104   * Returns {@code true} if this {@link Requirements} has no {@link
105   * Dependency} instances.
106   *
107   * @return {@code true} if this {@link Requirements} is empty;
108   * {@code false} otherwise
109   */
110  public final boolean isEmpty() {
111    return this.dependencies == null || this.dependencies.isEmpty();
112  }
113
114  /**
115   * Returns the {@link Collection} of {@link Dependency} instances
116   * comprising this {@link Requirements}.
117   *
118   * <p>This method may return {@code null}.</p>
119   *
120   * @see #setDependencies(Collection)
121   */
122  public final Collection<Dependency> getDependencies() {
123    return this.dependencies;
124  }
125
126  /**
127   * Installs the {@link Collection} of {@link Dependency} instances
128   * comprising this {@link Requirements}.
129   *
130   * @param dependencies the {@link Collection} of {@link Dependency}
131   * instances that will comprise this {@link Requirements}; may be
132   * {@code null}; not copied or cloned
133   *
134   * @see #getDependencies()
135   */
136  public final void setDependencies(final Collection<Dependency> dependencies) {
137    this.dependencies = dependencies;
138  }
139
140  private final void applyEnablementRules(final Map<String, Object> values) {
141    this.processTags(values);
142    this.processConditions(values);
143  }
144  
145  private final void processTags(final Map<String, Object> values) {
146    final Collection<Dependency> dependencies = this.getDependencies();
147    if (dependencies != null && !dependencies.isEmpty()) {
148      for (final Dependency dependency : dependencies) {
149        if (dependency != null) {
150          dependency.processTags(values);
151        }
152      }
153    }
154    
155  }
156
157  private final void processConditions(final Map<String, Object> values) {
158    final Collection<Dependency> dependencies = this.getDependencies();
159    if (dependencies != null && !dependencies.isEmpty()) {
160      for (final Dependency dependency : dependencies) {
161        if (dependency != null) {
162          dependency.processConditions(values);
163        }
164      }
165    }
166  }
167  
168
169  /*
170   * Static methods.
171   */
172
173
174  /**
175   * Applies rules around <a
176   * href="https://docs.helm.sh/developing_charts/#importing-child-values-via-requirements-yaml">importing
177   * subchart values into the parent chart's values</a>.
178   *
179   * @param chartBuilder the {@link Chart.Builder} to work on; must
180   * not be {@code null}
181   *
182   * @exception NullPointerException if {@code chartBuilder} is {@code
183   * null}
184   */
185  static final Chart.Builder processImportValues(final Chart.Builder chartBuilder) {
186    Objects.requireNonNull(chartBuilder);
187    final List<? extends Chart.Builder> flattenedCharts = Charts.flatten(chartBuilder);
188    if (flattenedCharts != null) {
189      assert !flattenedCharts.isEmpty();
190      final ListIterator<? extends Chart.Builder> listIterator = flattenedCharts.listIterator(flattenedCharts.size());
191      assert listIterator != null;
192      while (listIterator.hasPrevious()) {
193        final Chart.Builder chart = listIterator.previous();
194        assert chart != null;
195        processSingleChartImportValues(chart);
196      }
197    }
198    return chartBuilder;
199  }
200  
201  // Ported from requirements.go processImportValues().
202  private static final Chart.Builder processSingleChartImportValues(final Chart.Builder chartBuilder) {
203    Objects.requireNonNull(chartBuilder);
204
205    Chart.Builder returnValue = null;
206
207    final Map<String, Object> canonicalValues = Configs.toDefaultValuesMap(chartBuilder);
208    
209    Map<String, Object> combinedValues = new HashMap<>();
210    final Requirements requirements = fromChartOrBuilder(chartBuilder);
211    if (requirements != null) {
212      final Collection<Dependency> dependencies = requirements.getDependencies();
213      if (dependencies != null && !dependencies.isEmpty()) {
214        for (final Dependency dependency : dependencies) {
215          if (dependency != null) {
216            
217            final String dependencyName = dependency.getName();
218            if (dependencyName == null) {
219              throw new IllegalStateException();
220            }
221
222            final Collection<?> importValues = dependency.getImportValues();
223            if (importValues != null && !importValues.isEmpty()) {
224
225              // Not clear why we build this and install it later; it
226              // is never used.  See requirements.go's
227              // processImportValues().
228              final Collection<Object> newImportValues = new ArrayList<>(importValues.size());
229
230              for (final Object importValue : importValues) {
231                final String s;
232                
233                if (importValue instanceof Map) {
234                  @SuppressWarnings("unchecked")
235                  final Map<String, String> importValueMap = (Map<String, String>)importValue;
236                  
237                  final String importValueChild = importValueMap.get("child");
238                  final String importValueParent = importValueMap.get("parent");
239
240                  // Not clear to me why we build this and then
241                  // install it; it's never used in the .go code.
242                  final Map<String, String> newMap = new HashMap<>();
243                  newMap.put("child", importValueChild);
244                  newMap.put("parent", importValueParent);
245                  
246                  newImportValues.add(newMap);
247
248                  final Map<String, Object> vv =
249                    MapTree.newMapChain(importValueParent,
250                                        getMap(canonicalValues,
251                                               dependencyName + "." + importValueChild));
252                  combinedValues = Values.coalesceMaps(vv, canonicalValues);
253                  // OK
254                  
255                } else if (importValue instanceof String) {
256                  final String importValueString = (String)importValue;
257                  
258                  final String importValueChild = "exports." + importValueString;
259
260                  // Not clear to me why we build this and then
261                  // install it; it's never used in the .go code.
262                  final Map<String, String> newMap = new HashMap<>();
263                  newMap.put("child", importValueChild);
264                  newMap.put("parent", ".");
265                  
266                  newImportValues.add(newMap);
267                  
268                  combinedValues = Values.coalesceMaps(getMap(canonicalValues, dependencyName + "." + importValueChild), combinedValues);
269                  // OK
270                  
271                }
272              }
273              // The .go code alters the dependency's importValues;
274              // I'm not sure why, but we follow suit.
275              dependency.setImportValues(newImportValues);            
276            }
277          }
278        }
279      }
280    }
281    combinedValues = Values.coalesceMaps(canonicalValues, combinedValues);
282    assert combinedValues != null;
283    final String yaml = new Yaml().dump(combinedValues);
284    assert yaml != null;
285    final Config.Builder configBuilder = chartBuilder.getValuesBuilder();
286    assert configBuilder != null;
287    configBuilder.setRaw(yaml);
288    returnValue = chartBuilder;
289    assert returnValue != null;
290    return returnValue;
291  }
292  
293  // ported from Table() in chartutil/values.go
294  private static final Map<String, Object> getMap(Map<String, Object> map, final String dotSeparatedPath) {
295    final Map<String, Object> returnValue;
296    if (map == null || dotSeparatedPath == null || dotSeparatedPath.isEmpty() || map.isEmpty()) {
297      returnValue = null;
298    } else {
299      returnValue = new MapTree(map).getMap(dotSeparatedPath);
300    }
301    return returnValue;
302  }
303
304  // Ported from LoadRequirements() in chartutil/requirements.go
305  /**
306   * Creates a new {@link Requirements} from a top-level {@code
307   * requirements.yaml} {@linkplain Any resource} present in the
308   * supplied {@link ChartOrBuilder} and returns it.
309   *
310   * <p>This method may return {@code null} if the supplied {@link
311   * ChartOrBuilder} is itself {@code null} or doesn't have a {@code
312   * requirements.yaml} {@linkplain Any resource}.</p>
313   *
314   * @param chart the {@link ChartOrBuilder} housing a {@code
315   * requirement.yaml} {@linkplain Any resource}; may be {@code null}
316   * in which case {@code null} will be returned
317   *
318   * @return a new {@link Requirements} or {@code null}
319   */
320  public static final Requirements fromChartOrBuilder(final ChartOrBuilder chart) {
321    Requirements returnValue = null;
322    if (chart != null) {
323      final Collection<? extends Any> files = chart.getFilesList();
324      if (files != null && !files.isEmpty()) {
325        final Yaml yaml = new Yaml();
326        for (final Any file : files) {
327          if (file != null && "requirements.yaml".equals(file.getTypeUrl())) {
328            final ByteString fileContents = file.getValue();
329            if (fileContents != null) {
330              final String yamlString = fileContents.toStringUtf8();
331              if (yamlString != null) {
332                returnValue = yaml.loadAs(yamlString, Requirements.class);
333                assert returnValue != null;
334              }
335            }
336          }
337        }
338      }
339    }
340    return returnValue;
341  }
342
343  /**
344   * Applies a <a
345   * href="https://docs.helm.sh/developing_charts/#alias-field-in-requirements-yaml">variety
346   * of rules concerning subchart aliasing and enablement</a> to the
347   * contents of the supplied {@code Chart.Builder}.
348   *
349   * <p>This method never returns {@code null}
350   *
351   * @param chartBuilder the {@link Chart.Builder} whose subcharts may
352   * be affected; must not be {@code null}
353   *
354   * @param userSuppliedValues a {@link ConfigOrBuilder} representing
355   * overriding values; may be {@code null}
356   *
357   * @return the supplied {@code chartBuilder} for convenience; never
358   * {@code null}
359   *
360   * @exception NullPointerException if {@code chartBuilder} is {@code
361   * null}
362   */
363  public static final Chart.Builder apply(final Chart.Builder chartBuilder, ConfigOrBuilder userSuppliedValues) {
364    return apply(chartBuilder, userSuppliedValues, true /* top level, i.e. non-recursive call */);
365  }
366
367  /**
368   * Applies a <a
369   * href="https://docs.helm.sh/developing_charts/#alias-field-in-requirements-yaml">variety
370   * of rules concerning subchart aliasing and enablement</a> to the
371   * contents of the supplied {@code Chart.Builder}.
372   *
373   * <p>This method never returns {@code null}
374   *
375   * @param chartBuilder the {@link Chart.Builder} whose subcharts may
376   * be affected; must not be {@code null}
377   *
378   * @param userSuppliedValues a {@link ConfigOrBuilder} representing
379   * overriding values; may be {@code null}
380   *
381   * @param topLevel {@code true} if this is a non-recursive call, and
382   * hence certain "top-level" processing should take place
383   *
384   * @return the supplied {@code chartBuilder} for convenience; never
385   * {@code null}
386   *
387   * @exception NullPointerException if {@code chartBuilder} is {@code
388   * null}
389   */
390  static final Chart.Builder apply(final Chart.Builder chartBuilder, final ConfigOrBuilder userSuppliedValues, final boolean topLevel) {
391    Objects.requireNonNull(chartBuilder);
392
393    final Requirements requirements = fromChartOrBuilder(chartBuilder);
394    if (requirements != null && !requirements.isEmpty()) {
395      
396      final Collection<? extends Dependency> requirementsDependencies = requirements.getDependencies();
397      if (requirementsDependencies != null && !requirementsDependencies.isEmpty()) {
398        
399        final List<? extends Chart.Builder> existingSubcharts = chartBuilder.getDependenciesBuilderList();
400        if (existingSubcharts != null && !existingSubcharts.isEmpty()) { 
401
402          Collection<Dependency> missingSubcharts = null;
403          
404          for (final Dependency dependency : requirementsDependencies) {
405            if (dependency != null) {
406              boolean dependencySelectsAtLeastOneSubchart = false;
407              for (final Chart.Builder subchart : existingSubcharts) {
408                if (subchart != null) {
409                  dependencySelectsAtLeastOneSubchart = dependencySelectsAtLeastOneSubchart || dependency.selects(subchart);
410                  dependency.adjustName(subchart);
411                }
412              }
413              if (topLevel && !dependencySelectsAtLeastOneSubchart) {
414                if (missingSubcharts == null) {
415                  missingSubcharts = new ArrayList<>();
416                }
417                missingSubcharts.add(dependency);
418              } else {
419                dependency.setNameToAlias();
420              }
421              assert dependency.isEnabled();
422            }
423          }
424
425          if (missingSubcharts != null && !missingSubcharts.isEmpty()) {
426            throw new MissingDependenciesException(missingSubcharts);
427          }
428
429          // Combine the supplied values with the chart's default
430          // values in the form of a Map.
431          final Map<String, Object> chartValuesMap = Configs.toValuesMap(chartBuilder, userSuppliedValues);
432          assert chartValuesMap != null;
433          
434          // Now disable certain Dependencies.  This might be because
435          // the canonical value set contains tags or conditions
436          // designating them for disablement.  We couldn't disable
437          // them earlier because we didn't have values.
438          requirements.applyEnablementRules(chartValuesMap);
439
440          // Turn the values into YAML, because YAML is the only format
441          // we have for setting the contents of a new Config.Builder object (see
442          // Config.Builder#setRaw(String)).
443          final String userSuppliedValuesYaml;
444          if (chartValuesMap.isEmpty()) {
445            userSuppliedValuesYaml = "";
446          } else {
447            userSuppliedValuesYaml = new Yaml().dump(chartValuesMap);
448          }
449          assert userSuppliedValuesYaml != null;
450
451          final Config.Builder configBuilder = Config.newBuilder();
452          assert configBuilder != null;  
453          configBuilder.setRaw(userSuppliedValuesYaml);
454          
455          // Very carefully remove subcharts that have been disabled.
456          // Note the recursive call contained below.
457          ITERATION:
458          for (int i = 0; i < chartBuilder.getDependenciesCount(); i++) {
459            final Chart.Builder subchart = chartBuilder.getDependenciesBuilder(i);
460            for (final Dependency dependency : requirementsDependencies) {
461              if (dependency != null && !dependency.isEnabled() && dependency.selects(subchart)) {
462                chartBuilder.removeDependencies(i--);
463                continue ITERATION;
464              }
465            }
466            
467            // If we get here, this is an enabled subchart.
468            Requirements.apply(subchart, configBuilder, false /* not topLevel, i.e. this is recursive */); // <-- RECURSIVE CALL
469          }
470          
471        }
472      }
473    }
474    final Chart.Builder returnValue;
475    if (topLevel) {
476      returnValue = processImportValues(chartBuilder);
477    } else {
478      returnValue = chartBuilder;
479    }
480    return returnValue;
481  }
482
483  
484  /*
485   * Inner and nested classes.
486   */
487
488  
489  /**
490   * A {@link SimpleBeanInfo} describing the Java Bean properties for
491   * the {@link Dependency} class; not normally used directly by end
492   * users.
493   *
494   * @author <a href="https://about.me/lairdnelson/"
495   * target="_parent">Laird Nelson</a>
496   *
497   * @see SimpleBeanInfo
498   */
499  public static final class DependencyBeanInfo extends SimpleBeanInfo {
500
501
502    /*
503     * Instance methods.
504     */
505
506
507    /**
508     * The {@link Collection} of {@link PropertyDescriptor}s whose
509     * contents will be returned by the {@link
510     * #getPropertyDescriptors()} method.
511     *
512     * <p>This field is never {@code null}.</p>
513     *
514     * @see #getPropertyDescriptors() 
515     */
516    private final Collection<? extends PropertyDescriptor> propertyDescriptors;
517
518
519    /*
520     * Constructors.
521     */
522
523
524    /**
525     * Creates a new {@link DependencyBeanInfo}.
526     *
527     * @exception IntrospectionException if there was a problem
528     * creating a {@link PropertyDescriptor}
529     *
530     * @see #getPropertyDescriptors()
531     */
532    public DependencyBeanInfo() throws IntrospectionException {
533      super();
534      final Collection<PropertyDescriptor> propertyDescriptors = new ArrayList<>();
535      propertyDescriptors.add(new PropertyDescriptor("name", Dependency.class));
536      propertyDescriptors.add(new PropertyDescriptor("version", Dependency.class));
537      propertyDescriptors.add(new PropertyDescriptor("repository", Dependency.class));
538      propertyDescriptors.add(new PropertyDescriptor("condition", Dependency.class));
539      propertyDescriptors.add(new PropertyDescriptor("tags", Dependency.class));
540      propertyDescriptors.add(new PropertyDescriptor("import-values", Dependency.class, "getImportValues", "setImportValues"));
541      propertyDescriptors.add(new PropertyDescriptor("alias", Dependency.class));
542      this.propertyDescriptors = propertyDescriptors;
543    }
544
545
546    /*
547     * Instance methods.
548     */
549
550
551    /**
552     * Returns an array of {@link PropertyDescriptor}s describing the
553     * {@link Dependency} class.
554     *
555     * <p>This method never returns {@code null}.</p>
556     *
557     * @return a non-{@code null}, non-empty array of {@link
558     * PropertyDescriptor}s
559     */
560    @Override
561    public final PropertyDescriptor[] getPropertyDescriptors() {
562      return this.propertyDescriptors.toArray(new PropertyDescriptor[this.propertyDescriptors.size()]);
563    }
564    
565  }
566
567  /**
568   * A description of a subchart that should be present in a parent
569   * Helm chart; not normally used directly by end users.
570   *
571   * @author <a href="https://about.me/lairdnelson"
572   * target="_parent">Laird Nelson</a>
573   *
574   * @see Requirements
575   */
576  public static final class Dependency {
577
578
579    /*
580     * Static fields.
581     */
582    
583    
584    /**
585     * An unanchored {@link Pattern} matching a sequence of zero or more
586     * whitespace characters, followed by a comma, followed by zero or
587     * more whitespace characters.
588     *
589     * <p>This field is never {@code null}.</p>
590     *
591     * <p>This field is used during {@link #processConditions(Map)}
592     * method execution.</p>
593     */
594    private static final Pattern commaSplitPattern = Pattern.compile("\\s*,\\s*");
595    
596
597    /*
598     * Instance fields.
599     */
600
601
602    /**
603     * The name of the subchart being represented by this {@link
604     * Requirements.Dependency}.
605     *
606     * <p>This field may be {@code null}.</p>
607     *
608     * @see #getName()
609     *
610     * @see #setName(String)
611     */
612    private String name;
613
614    /**
615     * The range of acceptable semantic versions of the subchart being
616     * represented by this {@link Requirements.Dependency}.
617     *
618     * <p>This field may be {@code null}.</p>
619     *
620     * @see #getVersion()
621     *
622     * @see #setVersion(String)
623     */
624    private String versionRange;
625
626    /**
627     * A {@link String} representation of a URI which, when {@code
628     * index.yaml} is appended to it, results in a URI designating a
629     * Helm chart repository index.
630     *
631     * <p>This field may be {@code null}.</p>
632     *
633     * @see #getRepository()
634     *
635     * @see #setRepository(String)
636     */
637    private String repository;
638
639    /**
640     * A period-separated path that, when evaluated against a {@link
641     * Map} of {@link Map}s representing user-supplied or default
642     * values, will hopefully result in a value that can, in turn, be
643     * evaluated as a truth-value to aid in the enabling and disabling
644     * of subcharts.
645     *
646     * <p>This field may be {@code null}.</p>
647     *
648     * <p>This field may actually hold several such paths separated by
649     * commas.  This is an artifact of the design of Helm's {@code
650     * requirements.yaml} file.</p>
651     *
652     * @see #getCondition()
653     *
654     * @see #setCondition(String)
655     */
656    private String condition;
657
658    /**
659     * A {@link Collection} of tags that can be used to enable or
660     * disable subcharts.
661     *
662     * <p>This field may be {@code null}.
663     *
664     * @see #getTags()
665     *
666     * @see #setTags(Collection)
667     */
668    private Collection<String> tags;
669
670    /**
671     * Whether the subchart that this {@link Requirements.Dependency}
672     * identifies is to be considered enabled.
673     *
674     * <p>This field is set to {@code true} by default.</p>
675     *
676     * @see #isEnabled()
677     *
678     * @see #setEnabled(boolean)
679     */
680    private boolean enabled;
681
682    /**
683     * A {@link Collection} representing the contents of a {@code
684     * requirements.yaml}'s <a
685     * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code
686     * import-values} section</a>.
687     *
688     * <p>This field may be {@code null}.</p>
689     *
690     * @see #getImportValues()
691     *
692     * @see #setImportValues(Collection)
693     */
694    private Collection<Object> importValues;
695
696    /**
697     * The alias to use for the subchart identified by this {@link
698     * Requirements.Dependency}.
699     *
700     * <p>This field may be {@code null}.</p>
701     *
702     * @see #getAlias()
703     *
704     * @see #setAlias(String)
705     */
706    private String alias;
707
708
709    /*
710     * Constructors.
711     */
712
713
714    /**
715     * Creates a new {@link Dependency}.
716     */
717    public Dependency() {
718      super();
719      this.setEnabled(true);
720    }
721
722
723    /*
724     * Instance methods.
725     */
726    
727
728    /**
729     * Returns the name of the subchart being represented by this {@link
730     * Requirements.Dependency}.
731     *
732     * <p>This method may return {@code null}.</p>
733     *
734     * @return the name of the subchart being represented by this {@link
735     * Requirements.Dependency}, or {@code null}
736     *
737     * @see #setName(String)
738     */
739    public final String getName() {
740      return this.name;
741    }
742
743    /**
744     * Sets the name of the subchart being represented by this {@link
745     * Requirements.Dependency}.
746     *
747     * @param name the new name; may be {@code null}
748     *
749     * @see #getName()
750     */
751    public final void setName(final String name) {
752      this.name = name;
753    }
754
755    /**
756     * Returns the range of acceptable semantic versions of the
757     * subchart being represented by this {@link
758     * Requirements.Dependency}.
759     *
760     * <p>This method may return {@code null}.</p>
761     *
762     * @return the range of acceptable semantic versions of the
763     * subchart being represented by this {@link
764     * Requirements.Dependency}, or {@code null}
765     *
766     * @see #setVersion(String)
767     */
768    public final String getVersion() {
769      return this.versionRange;
770    }
771
772    /**
773     * Sets the range of acceptable semantic versions of the subchart
774     * being represented by this {@link Requirements.Dependency}.
775     *
776     * @param versionRange the new semantic version range; may be {@code
777     * null}
778     *
779     * @see #getVersion()
780     */
781    public final void setVersion(final String versionRange) {
782      this.versionRange = versionRange;
783    }
784
785    /**
786     * Returns the {@link String} representation of a URI which, when
787     * {@code index.yaml} is appended to it, results in a URI
788     * designating a Helm chart repository index.
789     *
790     * <p>This method may return {@code null}.</p>
791     *
792     * @return the {@link String} representation of a URI which, when
793     * {@code index.yaml} is appended to it, results in a URI
794     * designating a Helm chart repository index, or {@code null}
795     *
796     * @see #setRepository(String)
797     */
798    public final String getRepository() {
799      return this.repository;
800    }
801
802    /**
803     * Sets the {@link String} representation of a URI which, when
804     * {@code index.yaml} is appended to it, results in a URI
805     * designating a Helm chart repository index.
806     *
807     * @param repository the {@link String} representation of a URI
808     * which, when {@code index.yaml} is appended to it, results in a
809     * URI designating a Helm chart repository index, or {@code null}
810     *
811     * @see #getRepository()
812     */
813    public final void setRepository(final String repository) {
814      this.repository = repository;
815    }
816
817    /**
818     * Returns a period-separated path that, when evaluated against a
819     * {@link Map} of {@link Map}s representing user-supplied or
820     * default values, will hopefully result in a value that can, in
821     * turn, be evaluated as a truth-value to aid in the enabling and
822     * disabling of subcharts.
823     *
824     * <p>This method may return {@code null}.</p>
825     *
826     * <p>This method may return a value that actually holds several
827     * such paths separated by commas.  This is an artifact of the
828     * design of Helm's {@code requirements.yaml} file.</p>
829     *
830     * @return a period-separated path that, when evaluated against a
831     * {@link Map} of {@link Map}s representing user-supplied or
832     * default values, will hopefully result in a value that can, in
833     * turn, be evaluated as a truth-value to aid in the enabling and
834     * disabling of subcharts, or {@code null}
835     *
836     * @see #setCondition(String)
837     */
838    public final String getCondition() {
839      return this.condition;
840    }
841
842    /**
843     * Sets the period-separated path that, when evaluated against a
844     * {@link Map} of {@link Map}s representing user-supplied or
845     * default values, will hopefully result in a value that can, in
846     * turn, be evaluated as a truth-value to aid in the enabling and
847     * disabling of subcharts.
848     *
849     * @param condition a period-separated path that, when evaluated
850     * against a {@link Map} of {@link Map}s representing
851     * user-supplied or default values, will hopefully result in a
852     * value that can, in turn, be evaluated as a truth-value to aid
853     * in the enabling and disabling of subcharts, or {@code null}
854     *
855     * @see #getCondition()
856     */
857    public final void setCondition(final String condition) {
858      this.condition = condition;
859    }
860
861    /**
862     * Returns the {@link Collection} of tags that can be used to enable or
863     * disable subcharts.
864     *
865     * <p>This method may return {@code null}.</p>
866     *
867     * @return the {@link Collection} of tags that can be used to
868     * enable or disable subcharts, or {@code null}
869     *
870     * @see #setTags(Collection)
871     */
872    public final Collection<String> getTags() {
873      return this.tags;
874    }
875
876    /**
877     * Sets the {@link Collection} of tags that can be used to enable
878     * or disable subcharts.
879     *
880     * @param tags the {@link Collection} of tags that can be used to
881     * enable or disable subcharts; may be {@code null}
882     *
883     * @see #getTags()
884     */
885    public final void setTags(final Collection<String> tags) {
886      this.tags = tags;
887    }
888
889    /**
890     * Returns {@code true} if the subchart this {@link
891     * Requirements.Dependency} identifies is to be considered
892     * enabled.
893     *
894     * @return {@code true} if the subchart this {@link
895     * Requirements.Dependency} identifies is to be considered
896     * enabled; {@code false} otherwise
897     *
898     * @see #setEnabled(boolean)
899     */
900    public final boolean isEnabled() {
901      return this.enabled;
902    }
903
904    /**
905     * Sets whether the subchart this {@link
906     * Requirements.Dependency} identifies is to be considered
907     * enabled.
908     *
909     * @param enabled whether the subchart this {@link
910     * Requirements.Dependency} identifies is to be considered
911     * enabled
912     *
913     * @see #isEnabled()
914     */
915    public final void setEnabled(final boolean enabled) {
916      this.enabled = enabled;
917    }
918
919    /**
920     * Returns a {@link Collection} representing the contents of a {@code
921     * requirements.yaml}'s <a
922     * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code
923     * import-values} section</a>.
924     *
925     * <p>This method may return {@code null}.</p>
926     *
927     * @return a {@link Collection} representing the contents of a {@code
928     * requirements.yaml}'s <a
929     * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code
930     * import-values} section</a>, or {@code null}
931     *
932     * @see #setImportValues(Collection)
933     */
934    public final Collection<Object> getImportValues() {
935      return this.importValues;
936    }
937
938    /**
939     * Sets the {@link Collection} representing the contents of a {@code
940     * requirements.yaml}'s <a
941     * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code
942     * import-values} section</a>.
943     *
944     * @param importValues the {@link Collection} representing the contents of a {@code
945     * requirements.yaml}'s <a
946     * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code
947     * import-values} section</a>; may be {@code null}
948     *
949     * @see #getImportValues()
950     */
951    public final void setImportValues(final Collection<Object> importValues) {
952      this.importValues = importValues;
953    }
954
955    /**
956     * Returns the alias to use for the subchart identified by this {@link
957     * Requirements.Dependency}.
958     *
959     * <p>This method may return {@code null}.</p>
960     *
961     * @return the alias to use for the subchart identified by this {@link
962     * Requirements.Dependency}, or {@code null}
963     *
964     * @see #setAlias(String)
965     */
966    public final String getAlias() {
967      return this.alias;
968    }
969
970    /**
971     * Sets the alias to use for the subchart identified by this {@link
972     * Requirements.Dependency}.
973     *
974     * @param alias the alias to use for the subchart identified by this {@link
975     * Requirements.Dependency}; may be {@code null}
976     *
977     * @see #getAlias()
978     */
979    public final void setAlias(final String alias) {
980      this.alias = alias;
981    }
982
983    /**
984     * Returns {@code true} if this {@link Requirements.Dependency}
985     * identifies the given {@link ChartOrBuilder}.
986     *
987     * @param chart the {@link ChartOrBuilder} to check; may be {@code
988     * null} in which case {@code false} will be returned
989     *
990     * @return {@code true} if this {@link Requirements.Dependency}
991     * identifies the given {@link ChartOrBuilder}; {@code false}
992     * otherwise
993     */
994    public final boolean selects(final ChartOrBuilder chart) {
995      if (chart == null) {
996        return false;
997      }
998      return this.selects(chart.getMetadata());
999    }
1000    
1001    private final boolean selects(final MetadataOrBuilder metadata) {
1002      final boolean returnValue;
1003      if (metadata == null) {
1004        returnValue = false;
1005      } else {
1006        returnValue = this.selects(metadata.getName(), metadata.getVersion());
1007      }
1008      return returnValue;
1009    }
1010
1011    private final boolean selects(final String name, final String versionString) {
1012      if (versionString == null) {
1013        return false;
1014      }
1015      
1016      final Object myName = this.getName();
1017      if (myName == null) {
1018        if (name != null) {
1019          return false;
1020        }
1021      } else if (!myName.equals(name)) {
1022        return false;
1023      }
1024
1025      final String myVersionRange = this.getVersion();
1026      if (myVersionRange == null) {
1027        return false;
1028      }
1029      
1030      final Version version = Version.valueOf(versionString);
1031      assert version != null;
1032      return version.satisfies(ExpressionParser.newInstance().parse(myVersionRange));
1033    }
1034
1035    final boolean adjustName(final Chart.Builder subchart) {
1036      boolean returnValue = false;
1037      if (subchart != null && this.selects(subchart)) {
1038        final String alias = this.getAlias();
1039        if (alias != null && !alias.isEmpty() && subchart.hasMetadata()) {
1040          final Metadata.Builder subchartMetadataBuilder = subchart.getMetadataBuilder();
1041          assert subchartMetadataBuilder != null;
1042          if (!alias.equals(subchartMetadataBuilder.getName())) {
1043            // Rename the chart to have our alias as its new name.
1044            subchartMetadataBuilder.setName(alias);
1045            returnValue = true;
1046          }
1047        }
1048      }
1049      return returnValue;
1050    }
1051
1052    final boolean setNameToAlias() {
1053      boolean returnValue = false;
1054      final String alias = this.getAlias();
1055      if (alias != null && !alias.isEmpty() && !alias.equals(this.getName())) {        
1056        this.setName(alias);
1057        returnValue = true;
1058      }
1059      return returnValue;
1060    }
1061
1062    final void processTags(final Map<String, Object> values) {
1063      if (values != null) {
1064        final Object tagsObject = values.get("tags");
1065        if (tagsObject instanceof Map) {
1066          final Map<?, ?> tags = (Map<?, ?>)tagsObject;
1067          final Collection<? extends String> myTags = this.getTags();
1068          if (myTags != null && !myTags.isEmpty()) {
1069            boolean explicitlyTrue = false;
1070            boolean explicitlyFalse = false;
1071            for (final String myTag : myTags) {
1072              final Object tagValue = tags.get(myTag);
1073              if (Boolean.TRUE.equals(tagValue)) {
1074                explicitlyTrue = true;
1075              } else if (Boolean.FALSE.equals(tagValue)) {
1076                explicitlyFalse = true;
1077              } else {
1078                // Not a Boolean at all; just skip it
1079              }
1080            }
1081            
1082            // Note that this block looks different from the analogous
1083            // block in processConditions() below.  It is this way in the
1084            // Go code as well.
1085            if (explicitlyFalse) {
1086              if (!explicitlyTrue) {
1087                this.setEnabled(false);
1088              }
1089            } else {
1090              this.setEnabled(explicitlyTrue);
1091            }
1092          }
1093        }
1094      }
1095    }
1096    
1097    final void processConditions(final Map<String, Object> values) {
1098      if (values != null && !values.isEmpty()) {
1099        final MapTree mapTree = new MapTree(values);
1100        boolean explicitlyTrue = false;
1101        boolean explicitlyFalse = false;
1102        String conditionString = this.getCondition();
1103        if (conditionString != null) {
1104          conditionString = conditionString.trim();
1105          final String[] conditions = commaSplitPattern.split(conditionString);
1106          if (conditions != null && conditions.length > 0) {
1107            for (final String condition : conditions) {
1108              if (condition != null && !condition.isEmpty()) {
1109                final Object conditionValue = mapTree.get(condition, Object.class);
1110                if (Boolean.TRUE.equals(conditionValue)) {
1111                  explicitlyTrue = true;
1112                } else if (Boolean.FALSE.equals(conditionValue)) {
1113                  explicitlyFalse = true;
1114                } else if (conditionValue != null) {
1115                  break;
1116                }
1117              }
1118            }
1119          }
1120        }
1121        
1122        // Note that this block looks different from the analogous block
1123        // in processTags() above.  It is this way in the Go code as
1124        // well.
1125        if (explicitlyFalse) {
1126          if (!explicitlyTrue) {
1127            this.setEnabled(false);
1128          }
1129        } else if (explicitlyTrue) {
1130          this.setEnabled(true);
1131        }
1132      }
1133    }
1134
1135    /**
1136     * Returns a {@link String} representation of this {@link
1137     * Requirements.Dependency}.
1138     *
1139     * <p>This method never returns {@code null}.</p>
1140     *
1141     * @return a non-{@code null} {@link String} representation of
1142     * this {@link Requirements.Dependency}
1143     */
1144    @Override
1145    public final String toString() {
1146      final StringBuilder sb = new StringBuilder();
1147      final Object name = this.getName();
1148      if (name == null) {
1149        sb.append("Unnamed");
1150      } else {
1151        sb.append(name);
1152      }
1153      final String alias = this.getAlias();
1154      if (alias != null && !alias.isEmpty()) {
1155        sb.append(" (").append(alias).append(")");
1156      }
1157      sb.append(" ");
1158      sb.append(this.getVersion());
1159      return sb.toString();
1160    }
1161
1162  }
1163  
1164}