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);
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 firstInvocation {@code true} if this is a non-recursive
382   * call, and 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 version of the subchart being represented by this {@link
616     * 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 version;
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 version of the subchart being represented by this {@link
757     * Requirements.Dependency}.
758     *
759     * <p>This method may return {@code null}.</p>
760     *
761     * @return the version of the subchart being represented by this {@link
762     * Requirements.Dependency}, or {@code null}
763     *
764     * @see #setVersion(String)
765     */
766    public final String getVersion() {
767      return this.version;
768    }
769
770    /**
771     * Sets the version of the subchart being represented by this {@link
772     * Requirements.Dependency}.
773     *
774     * @param version the new version; may be {@code null}
775     *
776     * @see #getVersion()
777     */
778    public final void setVersion(final String version) {
779      this.version = version;
780    }
781
782    /**
783     * Returns the {@link String} representation of a URI which, when
784     * {@code index.yaml} is appended to it, results in a URI
785     * designating a Helm chart repository index.
786     *
787     * <p>This method may return {@code null}.</p>
788     *
789     * @return the {@link String} representation of a URI which, when
790     * {@code index.yaml} is appended to it, results in a URI
791     * designating a Helm chart repository index, or {@code null}
792     *
793     * @see #setRepository(String)
794     */
795    public final String getRepository() {
796      return this.repository;
797    }
798
799    /**
800     * Sets the {@link String} representation of a URI which, when
801     * {@code index.yaml} is appended to it, results in a URI
802     * designating a Helm chart repository index.
803     *
804     * @param repository the {@link String} representation of a URI
805     * which, when {@code index.yaml} is appended to it, results in a
806     * URI designating a Helm chart repository index, or {@code null}
807     *
808     * @see #getRepository()
809     */
810    public final void setRepository(final String repository) {
811      this.repository = repository;
812    }
813
814    /**
815     * Returns a period-separated path that, when evaluated against a
816     * {@link Map} of {@link Map}s representing user-supplied or
817     * default values, will hopefully result in a value that can, in
818     * turn, be evaluated as a truth-value to aid in the enabling and
819     * disabling of subcharts.
820     *
821     * <p>This method may return {@code null}.</p>
822     *
823     * <p>This method may return a value that actually holds several
824     * such paths separated by commas.  This is an artifact of the
825     * design of Helm's {@code requirements.yaml} file.</p>
826     *
827     * @return a period-separated path that, when evaluated against a
828     * {@link Map} of {@link Map}s representing user-supplied or
829     * default values, will hopefully result in a value that can, in
830     * turn, be evaluated as a truth-value to aid in the enabling and
831     * disabling of subcharts, or {@code null}
832     *
833     * @see #setCondition(String)
834     */
835    public final String getCondition() {
836      return this.condition;
837    }
838
839    /**
840     * Sets the period-separated path that, when evaluated against a
841     * {@link Map} of {@link Map}s representing user-supplied or
842     * default values, will hopefully result in a value that can, in
843     * turn, be evaluated as a truth-value to aid in the enabling and
844     * disabling of subcharts.
845     *
846     * @param condition a period-separated path that, when evaluated
847     * against a {@link Map} of {@link Map}s representing
848     * user-supplied or default values, will hopefully result in a
849     * value that can, in turn, be evaluated as a truth-value to aid
850     * in the enabling and disabling of subcharts, or {@code null}
851     *
852     * @see #getCondition()
853     */
854    public final void setCondition(final String condition) {
855      this.condition = condition;
856    }
857
858    /**
859     * Returns the {@link Collection} of tags that can be used to enable or
860     * disable subcharts.
861     *
862     * <p>This method may return {@code null}.</p>
863     *
864     * @return the {@link Collection} of tags that can be used to
865     * enable or disable subcharts, or {@code null}
866     *
867     * @see #setTags(Collection)
868     */
869    public final Collection<String> getTags() {
870      return this.tags;
871    }
872
873    /**
874     * Sets the {@link Collection} of tags that can be used to enable
875     * or disable subcharts.
876     *
877     * @param tags the {@link Collection} of tags that can be used to
878     * enable or disable subcharts; may be {@code null}
879     *
880     * @see #getTags()
881     */
882    public final void setTags(final Collection<String> tags) {
883      this.tags = tags;
884    }
885
886    /**
887     * Returns {@code true} if the subchart this {@link
888     * Requirements.Dependency} identifies is to be considered
889     * enabled.
890     *
891     * @return {@code true} if the subchart this {@link
892     * Requirements.Dependency} identifies is to be considered
893     * enabled; {@code false} otherwise
894     *
895     * @see #setEnabled(boolean)
896     */
897    public final boolean isEnabled() {
898      return this.enabled;
899    }
900
901    /**
902     * Sets whether the subchart this {@link
903     * Requirements.Dependency} identifies is to be considered
904     * enabled.
905     *
906     * @param enabled whether the subchart this {@link
907     * Requirements.Dependency} identifies is to be considered
908     * enabled
909     *
910     * @see #isEnabled()
911     */
912    public final void setEnabled(final boolean enabled) {
913      this.enabled = enabled;
914    }
915
916    /**
917     * Returns a {@link Collection} representing the contents of a {@code
918     * requirements.yaml}'s <a
919     * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code
920     * import-values} section</a>.
921     *
922     * <p>This method may return {@code null}.</p>
923     *
924     * @return a {@link Collection} representing the contents of a {@code
925     * requirements.yaml}'s <a
926     * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code
927     * import-values} section</a>, or {@code null}
928     *
929     * @see #setImportValues(Collection)
930     */
931    public final Collection<Object> getImportValues() {
932      return this.importValues;
933    }
934
935    /**
936     * Sets the {@link Collection} representing the contents of a {@code
937     * requirements.yaml}'s <a
938     * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code
939     * import-values} section</a>.
940     *
941     * @param importValues the {@link Collection} representing the contents of a {@code
942     * requirements.yaml}'s <a
943     * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code
944     * import-values} section</a>; may be {@code null}
945     *
946     * @see #getImportValues()
947     */
948    public final void setImportValues(final Collection<Object> importValues) {
949      this.importValues = importValues;
950    }
951
952    /**
953     * Returns the alias to use for the subchart identified by this {@link
954     * Requirements.Dependency}.
955     *
956     * <p>This method may return {@code null}.</p>
957     *
958     * @return the alias to use for the subchart identified by this {@link
959     * Requirements.Dependency}, or {@code null}
960     *
961     * @see #setAlias(String)
962     */
963    public final String getAlias() {
964      return this.alias;
965    }
966
967    /**
968     * Sets the alias to use for the subchart identified by this {@link
969     * Requirements.Dependency}.
970     *
971     * @param alias the alias to use for the subchart identified by this {@link
972     * Requirements.Dependency}; may be {@code null}
973     *
974     * @see #getAlias()
975     */
976    public final void setAlias(final String alias) {
977      this.alias = alias;
978    }
979
980    /**
981     * Returns {@code true} if this {@link Requirements.Dependency}
982     * identifies the given {@link ChartOrBuilder}.
983     *
984     * @param chart the {@link ChartOrBuilder} to check; may be {@code
985     * null} in which case {@code false} will be returned
986     *
987     * @return {@code true} if this {@link Requirements.Dependency}
988     * identifies the given {@link ChartOrBuilder}; {@code false}
989     * otherwise
990     */
991    public final boolean selects(final ChartOrBuilder chart) {
992      if (chart == null) {
993        return false;
994      }
995      return this.selects(chart.getMetadata());
996    }
997    
998    private final boolean selects(final MetadataOrBuilder metadata) {
999      final boolean returnValue;
1000      if (metadata == null) {
1001        returnValue = this.selects(null, null);
1002      } else {
1003        returnValue = this.selects(metadata.getName(), metadata.getVersion());
1004      }
1005      return returnValue;
1006    }
1007
1008    private final boolean selects(final String name, final String versionString) {
1009      final Object myName = this.getName();
1010      if (myName == null) {
1011        if (name != null) {
1012          return false;
1013        }
1014      } else if (!myName.equals(name)) {
1015        return false;
1016      }
1017
1018      final String myVersion = this.getVersion();
1019      if (myVersion == null) {
1020        if (versionString != null) {
1021          return false;
1022        }
1023      } else {
1024        final Version version = Version.valueOf(myVersion);
1025        assert version != null;
1026        final Parser<Expression> parser = ExpressionParser.newInstance();
1027        assert parser != null;
1028        final Expression semVerConstraint = parser.parse(versionString);
1029        assert semVerConstraint != null;
1030        if (!version.satisfies(semVerConstraint)) {
1031          return false;
1032        }
1033      }
1034      return true;
1035    }
1036
1037    final boolean adjustName(final Chart.Builder subchart) {
1038      boolean returnValue = false;
1039      if (subchart != null && this.selects(subchart)) {
1040        final String alias = this.getAlias();
1041        if (alias != null && !alias.isEmpty() && subchart.hasMetadata()) {
1042          final Metadata.Builder subchartMetadataBuilder = subchart.getMetadataBuilder();
1043          assert subchartMetadataBuilder != null;
1044          if (!alias.equals(subchartMetadataBuilder.getName())) {
1045            // Rename the chart to have our alias as its new name.
1046            subchartMetadataBuilder.setName(alias);
1047            returnValue = true;
1048          }
1049        }
1050      }
1051      return returnValue;
1052    }
1053
1054    final boolean setNameToAlias() {
1055      boolean returnValue = false;
1056      final String alias = this.getAlias();
1057      if (alias != null && !alias.isEmpty() && !alias.equals(this.getName())) {        
1058        this.setName(alias);
1059        returnValue = true;
1060      }
1061      return returnValue;
1062    }
1063
1064    final void processTags(final Map<String, Object> values) {
1065      if (values != null) {
1066        final Object tagsObject = values.get("tags");
1067        if (tagsObject instanceof Map) {
1068          final Map<?, ?> tags = (Map<?, ?>)tagsObject;
1069          final Collection<? extends String> myTags = this.getTags();
1070          if (myTags != null && !myTags.isEmpty()) {
1071            boolean explicitlyTrue = false;
1072            boolean explicitlyFalse = false;
1073            for (final String myTag : myTags) {
1074              final Object tagValue = tags.get(myTag);
1075              if (Boolean.TRUE.equals(tagValue)) {
1076                explicitlyTrue = true;
1077              } else if (Boolean.FALSE.equals(tagValue)) {
1078                explicitlyFalse = true;
1079              } else {
1080                // Not a Boolean at all; just skip it
1081              }
1082            }
1083            
1084            // Note that this block looks different from the analogous
1085            // block in processConditions() below.  It is this way in the
1086            // Go code as well.
1087            if (explicitlyFalse) {
1088              if (!explicitlyTrue) {
1089                this.setEnabled(false);
1090              }
1091            } else {
1092              this.setEnabled(explicitlyTrue);
1093            }
1094          }
1095        }
1096      }
1097    }
1098    
1099    final void processConditions(final Map<String, Object> values) {
1100      if (values != null && !values.isEmpty()) {
1101        final MapTree mapTree = new MapTree(values);
1102        boolean explicitlyTrue = false;
1103        boolean explicitlyFalse = false;
1104        String conditionString = this.getCondition();
1105        if (conditionString != null) {
1106          conditionString = conditionString.trim();
1107          final String[] conditions = commaSplitPattern.split(conditionString);
1108          if (conditions != null && conditions.length > 0) {
1109            for (final String condition : conditions) {
1110              if (condition != null && !condition.isEmpty()) {
1111                final Object conditionValue = mapTree.get(condition, Object.class);
1112                if (Boolean.TRUE.equals(conditionValue)) {
1113                  explicitlyTrue = true;
1114                } else if (Boolean.FALSE.equals(conditionValue)) {
1115                  explicitlyFalse = true;
1116                } else if (conditionValue != null) {
1117                  break;
1118                }
1119              }
1120            }
1121          }
1122        }
1123        
1124        // Note that this block looks different from the analogous block
1125        // in processTags() above.  It is this way in the Go code as
1126        // well.
1127        if (explicitlyFalse) {
1128          if (!explicitlyTrue) {
1129            this.setEnabled(false);
1130          }
1131        } else if (explicitlyTrue) {
1132          this.setEnabled(true);
1133        }
1134      }
1135    }
1136
1137    /**
1138     * Returns a {@link String} representation of this {@link
1139     * Requirements.Dependency}.
1140     *
1141     * <p>This method never returns {@code null}.</p>
1142     *
1143     * @return a non-{@code null} {@link String} representation of
1144     * this {@link Requirements.Dependency}
1145     */
1146    @Override
1147    public final String toString() {
1148      final StringBuilder sb = new StringBuilder();
1149      final Object name = this.getName();
1150      if (name == null) {
1151        sb.append("Unnamed");
1152      } else {
1153        sb.append(name);
1154      }
1155      final String alias = this.getAlias();
1156      if (alias != null && !alias.isEmpty()) {
1157        sb.append(" (").append(alias).append(")");
1158      }
1159      sb.append(" ");
1160      sb.append(this.getVersion());
1161      return sb.toString();
1162    }
1163
1164  }
1165  
1166}