View Javadoc
1   package org.metricshub.winrm;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * WinRM Java Client
6    * ჻჻჻჻჻჻
7    * Copyright (C) 2023 - 2026 MetricsHub
8    * ჻჻჻჻჻჻
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   *
13   *      http://www.apache.org/licenses/LICENSE-2.0
14   *
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
21   */
22  
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.LinkedHashMap;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Set;
32  import java.util.regex.Matcher;
33  import java.util.regex.Pattern;
34  import java.util.stream.Collectors;
35  import org.metricshub.winrm.exceptions.WqlQuerySyntaxException;
36  
37  public class WqlQuery {
38  
39  	/**
40  	 * Pattern to parse a WQL query
41  	 * <ul>
42  	 * <li>group(1) = SELECT ... FROM
43  	 * <li>group(2) = Properties in the SELECT statement (but not '*')
44  	 * <li>group(3) = ASSOCIATORS OF { object ID }
45  	 * <li>group(4) = class in the FROM statement
46  	 * <li>group(5) = Rest of the WQL statement (WHERE, etc.)
47  	 */
48  	private static final Pattern WQL_PATTERN = Pattern.compile(
49  		"^\\s*(SELECT\\s+(?:\\*|([a-z0-9._]+(?:\\s*,\\s*[a-z0-9._]+)*))\\s+FROM\\s+)?(?:((?:ASSOCIATORS|REFERENCES)\\s+OF\\s+\\{.*\\})|([a-z0-9_]+))(\\s+WHERE\\s*+.+)?\\s*$",
50  		Pattern.CASE_INSENSITIVE | Pattern.DOTALL
51  	);
52  
53  	private String wql;
54  	private List<String> selectedProperties;
55  	private Map<String, Set<String>> subPropertiesMap;
56  	private String cleanWql;
57  
58  	private WqlQuery(
59  		String wql,
60  		List<String> selectedProperties,
61  		Map<String, Set<String>> subPropertiesMap,
62  		String cleanWql
63  	) {
64  		this.wql = wql;
65  		this.selectedProperties = selectedProperties;
66  		this.subPropertiesMap = subPropertiesMap;
67  		this.cleanWql = cleanWql;
68  	}
69  
70  	/**
71  	 * Parses the specified WQL query and returns a new instance of WqlQuery
72  	 * Supported WQL syntaxes:
73  	 * <ul>
74  	 * <li>SELECT * FROM Class
75  	 * <li>SELECT PropA, PropB FROM Class
76  	 * <li>SELECT PropA, PropB FROM Class WHERE condition
77  	 * <li>ASSOCIATORS OF { objectId }
78  	 * <li>ASSOCIATORS OF { objectId } WHERE condition
79  	 * <li>SELECT * FROM ASSOCIATORS OF { objectId } WHERE condition
80  	 * <li>SELECT PropA, PropB FROM ASSOCIATORS OF { objectId } WHERE condition
81  	 * </ul>
82  	 *
83  	 * @param wql The WQL query to parse
84  	 * @return a new WqlQuery instance
85  	 * @throws WqlQuerySyntaxException when the specified WQL is invalid and cannot be parsed
86  	 * @throws IllegalArgumentException if wql is null
87  	 */
88  	public static WqlQuery newInstance(CharSequence wql) throws WqlQuerySyntaxException {
89  		Utils.checkNonNull(wql, "wql");
90  
91  		Matcher wqlMatcher = WQL_PATTERN.matcher(wql);
92  
93  		// No match: invalid WQL
94  		if (!wqlMatcher.find()) {
95  			throw new WqlQuerySyntaxException(wql.toString());
96  		}
97  
98  		// Extract the different fragments of the parsed WQL
99  		String selectFragment = wqlMatcher.group(1);
100 		String propertiesFragment = wqlMatcher.group(2);
101 		String associatorsFragment = wqlMatcher.group(3);
102 		String classFragment = wqlMatcher.group(4);
103 		String restFragment = wqlMatcher.group(5);
104 
105 		// If there is no `SELECT` and no `ASSOCIATORS OF`, it's no valid WQL
106 		if (selectFragment == null && associatorsFragment == null) {
107 			throw new WqlQuerySyntaxException(wql.toString());
108 		}
109 
110 		List<String> properties = buildSelectedProperties(propertiesFragment);
111 		Map<String, Set<String>> subPropertiesMap = buildSupPropertiesMap(properties);
112 		String cleanWql = buildCleanWql(associatorsFragment, subPropertiesMap, classFragment, restFragment);
113 
114 		return new WqlQuery(wql.toString(), properties, subPropertiesMap, cleanWql);
115 	}
116 
117 	/**
118 	 * Note: All properties are converted to lower case
119 	 *
120 	 * @param propertiesFragment Comma-separated list of properties
121 	 * @return a cleaned-up array of the properties
122 	 */
123 	static List<String> buildSelectedProperties(String propertiesFragment) {
124 		if (Utils.isNotBlank(propertiesFragment)) {
125 			return Arrays.asList(propertiesFragment.trim().toLowerCase().split("\\s*,\\s*"));
126 		}
127 		return new ArrayList<>();
128 	}
129 
130 	/**
131 	 * Build a Map of subproperties to retrieve inside properties
132 	 * Example:
133 	 * Input:
134 	 * <code>PropA, PropB.Sub1, PropB.Sub2</code>
135 	 * Output:
136 	 * <ul>
137 	 * <li>PropA => emptySet()
138 	 * <li>PropB => { "Sub1", "Sub2" }
139 	 * </ul>
140 	 *
141 	 * @param properties Selected properties (that may include subproperties)
142 	 * @return The map as described above
143 	 */
144 	static Map<String, Set<String>> buildSupPropertiesMap(final List<String> properties) {
145 		// Empty or null?
146 		if (properties == null || properties.isEmpty()) {
147 			return new HashMap<>();
148 		}
149 
150 		Map<String, Set<String>> subPropertiesMap = new LinkedHashMap<>();
151 		properties
152 			.stream()
153 			.filter(Utils::isNotBlank)
154 			.forEachOrdered(property -> {
155 				// Split the property into fragments:
156 				// propA => ["propA"]
157 				// propA.subProp => ["propA", "subProp"]
158 				String[] propertyFragmentArray = property.toLowerCase().split("\\.", 2);
159 				String mainProperty = propertyFragmentArray[0];
160 				String subProperty = propertyFragmentArray.length == 2 ? propertyFragmentArray[1] : null;
161 
162 				// Add this entry to the map
163 				subPropertiesMap.compute(
164 					mainProperty,
165 					(key, subPropertiesSet) -> {
166 						if (subPropertiesSet == null) {
167 							subPropertiesSet = new HashSet<>();
168 						}
169 						if (subProperty != null) {
170 							subPropertiesSet.add(subProperty);
171 						}
172 						return subPropertiesSet;
173 					}
174 				);
175 			});
176 
177 		return subPropertiesMap;
178 	}
179 
180 	/**
181 	 * Build a strict WQL query from the "dirty" one we have
182 	 * By <em>strict</em> we mean a syntax that can be executed by the WMI provider. <br>
183 	 * By <em>dirty</em> we mean the extra sugar-coated syntax we're allowing in Metricshub products,
184 	 * like subproperties, and <code>SELECT prop FROM ASSOCIATORS OF...</code>
185 	 * Examples:
186 	 * <ul>
187 	 * <li><code>SELECT PropA.Name FROM Win32_Class</code><br>
188 	 * => <b>SELECT PropA FROM Win32_Class</b>
189 	 * <li><code>SELECT Temperature FROM ASSOCIATORS OF { Win32_Class.Id=1 }</code><br>
190 	 * => <b>ASSOCIATORS OF { Win32_Class.Id=1 }</b>
191 	 * </ul>
192 	 *
193 	 * @param associatorsFragment The extracted ASSOCIATORS OF... fragment
194 	 * @param subPropertiesMap The map built with {@link WqlQuery#buildSupPropertiesMap(String[])}
195 	 * @param classFragment The extracted class fragment
196 	 * @param restFragment The rest (WHERE...)
197 	 * @return a clean and strict WQL statement
198 	 */
199 	static String buildCleanWql(
200 		String associatorsFragment,
201 		Map<String, Set<String>> subPropertiesMap,
202 		String classFragment,
203 		String restFragment
204 	) {
205 		String cleanWql;
206 
207 		if (associatorsFragment == null) {
208 			if (subPropertiesMap.keySet().isEmpty()) {
209 				cleanWql = "SELECT * FROM " + classFragment;
210 			} else {
211 				cleanWql = String.format(
212 					"SELECT %s FROM %s",
213 					subPropertiesMap.keySet().stream().collect(Collectors.joining(",")),
214 					classFragment
215 				);
216 			}
217 		} else {
218 			cleanWql = associatorsFragment;
219 		}
220 		if (restFragment != null) {
221 			cleanWql = cleanWql + restFragment;
222 		}
223 		return cleanWql;
224 	}
225 
226 	/**
227 	 * Get the properties of the SELECT statement, in lower case.
228 	 *
229 	 * @return an unmodifiable view of the selected properties (empty for {@code SELECT *})
230 	 */
231 	public List<String> getSelectedProperties() {
232 		return Collections.unmodifiableList(selectedProperties);
233 	}
234 
235 	/**
236 	 * Get the map of subproperties to retrieve inside each selected property, in lower case.
237 	 *
238 	 * @return an unmodifiable view of the property to subproperties map (the subproperty sets are
239 	 *         unmodifiable too)
240 	 */
241 	public Map<String, Set<String>> getSubPropertiesMap() {
242 		// Deep view: wrap each value set too, so callers cannot alter the parsed query's metadata
243 		final Map<String, Set<String>> view = new LinkedHashMap<>();
244 		subPropertiesMap
245 			.forEach((property, subProperties) -> view.put(property, Collections.unmodifiableSet(subProperties)));
246 		return Collections.unmodifiableMap(view);
247 	}
248 
249 	public String getCleanWql() {
250 		return cleanWql;
251 	}
252 
253 	@Override
254 	public String toString() {
255 		return wql;
256 	}
257 }