001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  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 implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.config.internal;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.util.*;
023
024import org.apache.juneau.config.format.*;
025import org.apache.juneau.config.store.*;
026
027/**
028 * Merges a base configuration with one or more profile overlays into a single internal-INI string.
029 *
030 * <p>
031 * This is the engine behind config <b>profiles</b> ({@code <name>-<profile>.cfg}/{@code .yml} overlays activated via
032 * {@code juneau.profiles.active}).  The base config is the foundation; each active profile's entries are layered on
033 * top in activation order so that:
034 * <ul>
035 *    <li><b>profile wins over base</b> for any section/key the profile redefines, and
036 *    <li><b>last-active-profile wins</b> when multiple active profiles redefine the same section/key.
037 * </ul>
038 *
039 * <p>
040 * Each input (base + profiles) is parsed through its own {@link ConfigFormat} (so a YAML profile is converted to the
041 * internal INI form like any other config), then the parsed maps are overlaid entry-by-entry and re-emitted as a
042 * single internal-INI string via {@link ConfigMap#asIniString()}.  Feeding that merged string back through the normal
043 * {@link ConfigMap} parse keeps format handling, variable resolution, and change-listener wiring identical to a
044 * single-file config &mdash; profiles are transparent to everything downstream of the store.
045 *
046 * @since 10.0.0
047 */
048public final class ProfileMerge {
049
050   private ProfileMerge() {}
051
052   /**
053    * Merges base + profile contents into one internal-INI string (profile-wins, last-active-profile-wins).
054    *
055    * @param store The store the throwaway parse maps are associated with (used only for parse context; never written).
056    * @param baseName The base config name (for parse diagnostics).
057    * @param baseContents
058    *    The raw base config contents (format-native).
059    *    <br>Can be <jk>null</jk> (treated as empty contents).
060    * @param profiles
061    *    Ordered profile overlays (activation order); each is a raw format-native contents string.
062    *    <br>May be empty, but must not be <jk>null</jk>.
063    * @param format
064    *    The config format used to parse every input.
065    *    <br>Can be <jk>null</jk> (defaults to {@link IniConfigFormat}).
066    * @return The merged contents as an internal-INI string.
067    * @throws IOException If any input fails to parse.
068    */
069   public static String merge(ConfigStore store, String baseName, String baseContents, List<String> profiles, ConfigFormat format) throws IOException {
070      var f = format == null ? IniConfigFormat.INSTANCE : format;
071      // Parse the base into a throwaway map; this becomes the accumulator we overlay onto.
072      var merged = new ConfigMap(store, baseName, baseContents == null ? "" : baseContents, f);
073      for (var p : profiles) {
074         if (ib(p))
075            continue;
076         var overlay = new ConfigMap(store, baseName, p, f);
077         applyOverlay(merged, overlay);
078      }
079      return merged.asIniString();
080   }
081
082   /**
083    * Overlays every section/key entry from {@code overlay} onto {@code base} (overlay wins).
084    */
085   private static void applyOverlay(ConfigMap base, ConfigMap overlay) {
086      for (var section : overlay.getSections()) {
087         // Ensure the section exists on the base (preserving the overlay's section pre-lines when it is new).
088         if (! base.hasSection(section))
089            base.setSection(section, overlay.getPreLines(section));
090         for (var key : overlay.getKeys(section)) {
091            var e = overlay.getEntry(section, key);
092            if (e != null)  // HTT: getKeys() only returns keys with entries, so getEntry is never null here.
093               base.setEntry(section, key, e.getValue(), e.getModifiers(), e.getComment(), e.getPreLines());
094         }
095      }
096   }
097}