View Javadoc
1   package org.metricshub.winrm;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * WinRM Java Client
6    * ჻჻჻჻჻჻
7    * Copyright 2023 - 2024 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.HashMap;
26  import java.util.HashSet;
27  import java.util.LinkedHashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.regex.Matcher;
32  import java.util.regex.Pattern;
33  import java.util.stream.Collectors;
34  import org.metricshub.winrm.exceptions.WqlQuerySyntaxException;
35  
36  public class WqlQuery {
37  
38  	/**
39  	 * Pattern to parse a WQL query
40  	 * <ul>
41  	 * <li>group(1) = SELECT ... FROM
42  	 * <li>group(2) = Properties in the SELECT statement (but not '*')
43  	 * <li>group(3) = ASSOCIATORS OF { object ID }
44  	 * <li>group(4) = class in the FROM statement
45  	 * <li>group(5) = Rest of the WQL statement (WHERE, etc.)
46  	 */
47  	private static final Pattern WQL_PATTERN = Pattern.compile(
48  		"^\\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*$",
49  		Pattern.CASE_INSENSITIVE | Pattern.DOTALL
50  	);
51  
52  	private String wql;
53  	private List<String> selectedProperties;
54  	private Map<String, Set<String>> subPropertiesMap;
55  	private String cleanWql;
56  
57  	private WqlQuery(
58  		String wql,
59  		List<String> selectedProperties,
60  		Map<String, Set<String>> subPropertiesMap,
61  		String cleanWql
62  	) {
63  		this.wql = wql;
64  		this.selectedProperties = selectedProperties;
65  		this.subPropertiesMap = subPropertiesMap;
66  		this.cleanWql = cleanWql;
67  	}
68  
69  	/**
70  	 * Parses the specified WQL query and returns a new instance of WqlQuery
71  	 *
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  	 * @param wql The WQL query to parse
83  	 * @return a new WqlQuery instance
84  	 * @throws WqlQuerySyntaxException when the specified WQL is invalid and cannot be parsed
85  	 * @throws IllegalArgumentException if wql is null
86  	 */
87  	public static WqlQuery newInstance(CharSequence wql) throws WqlQuerySyntaxException {
88  		Utils.checkNonNull(wql, "wql");
89  
90  		Matcher wqlMatcher = WQL_PATTERN.matcher(wql);
91  
92  		// No match: invalid WQL
93  		if (!wqlMatcher.find()) {
94  			throw new WqlQuerySyntaxException(wql.toString());
95  		}
96  
97  		// Extract the different fragments of the parsed WQL
98  		String selectFragment = wqlMatcher.group(1);
99  		String propertiesFragment = wqlMatcher.group(2);
100 		String associatorsFragment = wqlMatcher.group(3);
101 		String classFragment = wqlMatcher.group(4);
102 		String restFragment = wqlMatcher.group(5);
103 
104 		// If there is no `SELECT` and no `ASSOCIATORS OF`, it's no valid WQL
105 		if (selectFragment == null && associatorsFragment == null) {
106 			throw new WqlQuerySyntaxException(wql.toString());
107 		}
108 
109 		List<String> properties = buildSelectedProperties(propertiesFragment);
110 		Map<String, Set<String>> subPropertiesMap = buildSupPropertiesMap(properties);
111 		String cleanWql = buildCleanWql(associatorsFragment, subPropertiesMap, classFragment, restFragment);
112 
113 		return new WqlQuery(wql.toString(), properties, subPropertiesMap, cleanWql);
114 	}
115 
116 	/**
117 	 * Note: All properties are converted to lower case
118 	 * @param propertiesFragment Comma-separated list of properties
119 	 * @return a cleaned-up array of the properties
120 	 */
121 	static List<String> buildSelectedProperties(String propertiesFragment) {
122 		if (Utils.isNotBlank(propertiesFragment)) {
123 			return Arrays.asList(propertiesFragment.trim().toLowerCase().split("\\s*,\\s*"));
124 		}
125 		return new ArrayList<>();
126 	}
127 
128 	/**
129 	 * Build a Map of subproperties to retrieve inside properties
130 	 *
131 	 * Example:
132 	 *
133 	 * Input:
134 	 * <code>PropA, PropB.Sub1, PropB.Sub2</code>
135 	 *
136 	 * Output:
137 	 * <ul>
138 	 * <li>PropA => emptySet()
139 	 * <li>PropB => { "Sub1", "Sub2" }
140 	 * </ul>
141 	 *
142 	 * @param properties Selected properties (that may include subproperties)
143 	 * @return The map as described above
144 	 */
145 	static Map<String, Set<String>> buildSupPropertiesMap(final List<String> properties) {
146 		// Empty or null?
147 		if (properties == null || properties.isEmpty()) {
148 			return new HashMap<>();
149 		}
150 
151 		Map<String, Set<String>> subPropertiesMap = new LinkedHashMap<>();
152 		properties
153 			.stream()
154 			.filter(Utils::isNotBlank)
155 			.forEachOrdered(property -> {
156 				// Split the property into fragments:
157 				// propA => ["propA"]
158 				// propA.subProp => ["propA", "subProp"]
159 				String[] propertyFragmentArray = property.toLowerCase().split("\\.", 2);
160 				String mainProperty = propertyFragmentArray[0];
161 				String subProperty = propertyFragmentArray.length == 2 ? propertyFragmentArray[1] : null;
162 
163 				// Add this entry to the map
164 				subPropertiesMap.compute(
165 					mainProperty,
166 					(key, subPropertiesSet) -> {
167 						if (subPropertiesSet == null) {
168 							subPropertiesSet = new HashSet<>();
169 						}
170 						if (subProperty != null) {
171 							subPropertiesSet.add(subProperty);
172 						}
173 						return subPropertiesSet;
174 					}
175 				);
176 			});
177 
178 		return subPropertiesMap;
179 	}
180 
181 	/**
182 	 * Build a strict WQL query from the "dirty" one we have
183 	 *
184 	 * By <em>strict</em> we mean a syntax that can be executed by the WMI provider. <br>
185 	 * By <em>dirty</em> we mean the extra sugar-coated syntax we're allowing in Metricshub products,
186 	 * like subproperties, and <code>SELECT prop FROM ASSOCIATORS OF...</code>
187 	 *
188 	 * Examples:
189 	 * <ul>
190 	 * <li><code>SELECT PropA.Name FROM Win32_Class</code><br>
191 	 *  => <b>SELECT PropA FROM Win32_Class</b>
192 	 * <li><code>SELECT Temperature FROM ASSOCIATORS OF { Win32_Class.Id=1 }</code><br>
193 	 *  => <b>ASSOCIATORS OF { Win32_Class.Id=1 }</b>
194 	 * </ul>
195 	 * @param associatorsFragment The extracted ASSOCIATORS OF... fragment
196 	 * @param subPropertiesMap The map built with {@link WqlQuery#buildSupPropertiesMap(String[])}
197 	 * @param classFragment The extracted class fragment
198 	 * @param restFragment The rest (WHERE...)
199 	 * @return a clean and strict WQL statement
200 	 */
201 	static String buildCleanWql(
202 		String associatorsFragment,
203 		Map<String, Set<String>> subPropertiesMap,
204 		String classFragment,
205 		String restFragment
206 	) {
207 		String cleanWql;
208 
209 		if (associatorsFragment == null) {
210 			if (subPropertiesMap.keySet().isEmpty()) {
211 				cleanWql = "SELECT * FROM " + classFragment;
212 			} else {
213 				cleanWql =
214 					String.format(
215 						"SELECT %s FROM %s",
216 						subPropertiesMap.keySet().stream().collect(Collectors.joining(",")),
217 						classFragment
218 					);
219 			}
220 		} else {
221 			cleanWql = associatorsFragment;
222 		}
223 		if (restFragment != null) {
224 			cleanWql = cleanWql + restFragment;
225 		}
226 		return cleanWql;
227 	}
228 
229 	public List<String> getSelectedProperties() {
230 		return selectedProperties;
231 	}
232 
233 	public Map<String, Set<String>> getSubPropertiesMap() {
234 		return subPropertiesMap;
235 	}
236 
237 	public String getCleanWql() {
238 		return cleanWql;
239 	}
240 
241 	@Override
242 	public String toString() {
243 		return wql;
244 	}
245 }