001/*
002 * Copyright 2025 devteam@scivics-lab.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing,
011 * software distributed under the License is distributed on an
012 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
013 * either express or implied.  See the License for the
014 * specific language governing permissions and limitations
015 * under the License.
016 */
017
018package com.scivicslab.actoriac;
019
020import java.io.BufferedReader;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.InputStreamReader;
024import java.util.ArrayList;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028
029/**
030 * Parser for Ansible inventory files in INI format.
031 *
032 * <p>This parser supports basic Ansible inventory file format with groups
033 * and variables. Example:</p>
034 *
035 * <pre>
036 * [webservers]
037 * web1.example.com
038 * web2.example.com
039 *
040 * [dbservers]
041 * db1.example.com
042 *
043 * [all:vars]
044 * ansible_user=admin
045 * ansible_port=22
046 * </pre>
047 *
048 * @author devteam@scivics-lab.com
049 */
050public class InventoryParser {
051
052    /**
053     * Parses an Ansible inventory file.
054     *
055     * @param input the input stream of the inventory file
056     * @return the parsed inventory
057     * @throws IOException if reading the file fails
058     */
059    public static Inventory parse(InputStream input) throws IOException {
060        Inventory inventory = new Inventory();
061
062        try (BufferedReader reader = new BufferedReader(new InputStreamReader(input))) {
063            String currentGroup = null;
064            boolean inVarsSection = false;
065            Map<String, String> currentVars = new HashMap<>();
066
067            String line;
068            while ((line = reader.readLine()) != null) {
069                line = line.trim();
070
071                // Skip empty lines and comments
072                if (line.isEmpty() || line.startsWith("#") || line.startsWith(";")) {
073                    continue;
074                }
075
076                // Check for group header
077                if (line.startsWith("[") && line.endsWith("]")) {
078                    String groupDeclaration = line.substring(1, line.length() - 1);
079
080                    // Check if it's a vars section
081                    if (groupDeclaration.endsWith(":vars")) {
082                        inVarsSection = true;
083                        currentGroup = groupDeclaration.substring(0, groupDeclaration.length() - 5);
084                        currentVars = new HashMap<>();
085                    } else {
086                        inVarsSection = false;
087                        currentGroup = groupDeclaration;
088                        inventory.addGroup(currentGroup);
089                    }
090                    continue;
091                }
092
093                // Process content based on section type
094                if (inVarsSection) {
095                    // Parse variable assignment
096                    int equalsIndex = line.indexOf('=');
097                    if (equalsIndex > 0) {
098                        String key = line.substring(0, equalsIndex).trim();
099                        String value = line.substring(equalsIndex + 1).trim();
100                        currentVars.put(key, value);
101
102                        // Apply vars to group
103                        if ("all".equals(currentGroup)) {
104                            inventory.addGlobalVar(key, value);
105                        } else if (currentGroup != null) {
106                            inventory.addGroupVar(currentGroup, key, value);
107                        }
108                    }
109                } else if (currentGroup != null) {
110                    // Parse host line with optional variables
111                    // Format: hostname [key=value key=value ...]
112                    String[] tokens = line.split("\\s+");
113                    String hostname = tokens[0];
114                    inventory.addHost(currentGroup, hostname);
115
116                    // Parse host-specific variables
117                    for (int i = 1; i < tokens.length; i++) {
118                        String token = tokens[i];
119                        int equalsIndex = token.indexOf('=');
120                        if (equalsIndex > 0) {
121                            String key = token.substring(0, equalsIndex).trim();
122                            String value = token.substring(equalsIndex + 1).trim();
123                            inventory.addHostVar(hostname, key, value);
124                        }
125                    }
126                }
127            }
128        }
129
130        return inventory;
131    }
132
133    /**
134     * Represents a parsed Ansible inventory.
135     */
136    public static class Inventory {
137        private final Map<String, List<String>> groups = new HashMap<>();
138        private final Map<String, String> globalVars = new HashMap<>();
139        private final Map<String, Map<String, String>> groupVars = new HashMap<>();
140        private final Map<String, Map<String, String>> hostVars = new HashMap<>();
141
142        public void addGroup(String groupName) {
143            groups.putIfAbsent(groupName, new ArrayList<>());
144        }
145
146        public void addHost(String groupName, String hostname) {
147            groups.computeIfAbsent(groupName, k -> new ArrayList<>()).add(hostname);
148        }
149
150        public void addGlobalVar(String key, String value) {
151            globalVars.put(key, value);
152        }
153
154        public void addGroupVar(String groupName, String key, String value) {
155            groupVars.computeIfAbsent(groupName, k -> new HashMap<>()).put(key, value);
156        }
157
158        public void addHostVar(String hostname, String key, String value) {
159            hostVars.computeIfAbsent(hostname, k -> new HashMap<>()).put(key, value);
160        }
161
162        public List<String> getHosts(String groupName) {
163            return groups.getOrDefault(groupName, new ArrayList<>());
164        }
165
166        public Map<String, String> getGlobalVars() {
167            return new HashMap<>(globalVars);
168        }
169
170        public Map<String, String> getGroupVars(String groupName) {
171            return new HashMap<>(groupVars.getOrDefault(groupName, new HashMap<>()));
172        }
173
174        public Map<String, String> getHostVars(String hostname) {
175            return new HashMap<>(hostVars.getOrDefault(hostname, new HashMap<>()));
176        }
177
178        public Map<String, List<String>> getAllGroups() {
179            return new HashMap<>(groups);
180        }
181    }
182}