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 — 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}