View Javadoc
1   package org.metricshub.wmi.wbem;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * WMI Java Client
6    * ჻჻჻჻჻჻
7    * Copyright (C) 2023 - 2025 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 com.sun.jna.platform.win32.COM.COMUtils;
24  import com.sun.jna.platform.win32.COM.IUnknown;
25  import com.sun.jna.platform.win32.COM.Unknown;
26  import com.sun.jna.platform.win32.COM.Wbemcli;
27  import com.sun.jna.platform.win32.COM.Wbemcli.IWbemClassObject;
28  import com.sun.jna.platform.win32.Guid.REFIID;
29  import com.sun.jna.platform.win32.OaIdl.SAFEARRAY;
30  import com.sun.jna.platform.win32.OleAuto;
31  import com.sun.jna.platform.win32.Variant.VARIANT.ByReference;
32  import com.sun.jna.platform.win32.WinNT.HRESULT;
33  import com.sun.jna.ptr.IntByReference;
34  import com.sun.jna.ptr.PointerByReference;
35  import java.time.OffsetDateTime;
36  import java.util.AbstractMap;
37  import java.util.ArrayList;
38  import java.util.Collections;
39  import java.util.HashMap;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.Map.Entry;
43  import java.util.Optional;
44  import java.util.Set;
45  import java.util.function.Function;
46  import java.util.stream.Collectors;
47  import java.util.stream.Stream;
48  import org.metricshub.wmi.Utils;
49  
50  public class WmiCimTypeHandler {
51  
52  	/**
53  	 * Private constructor, as this class cannot be instantiated (it's pure static)
54  	 */
55  	private WmiCimTypeHandler() {}
56  
57  	/**
58  	 * Map of functions that convert WBEM types to Java corresponding class/type
59  	 */
60  	private static final Map<Integer, Function<ByReference, Object>> CIMTYPE_TO_CONVERTER_MAP;
61  
62  	static {
63  		final Map<Integer, Function<ByReference, Object>> map = new HashMap<>();
64  
65  		map.put(Wbemcli.CIM_EMPTY, value -> null);
66  
67  		map.put(Wbemcli.CIM_BOOLEAN, ByReference::booleanValue);
68  
69  		map.put(Wbemcli.CIM_UINT8, ByReference::byteValue);
70  		map.put(Wbemcli.CIM_UINT16, ByReference::intValue);
71  		map.put(Wbemcli.CIM_UINT32, ByReference::intValue);
72  		map.put(Wbemcli.CIM_UINT64, ByReference::stringValue);
73  		map.put(Wbemcli.CIM_SINT8, ByReference::shortValue);
74  		map.put(Wbemcli.CIM_SINT16, ByReference::shortValue);
75  		map.put(Wbemcli.CIM_SINT32, ByReference::intValue);
76  		map.put(Wbemcli.CIM_SINT64, ByReference::stringValue);
77  
78  		map.put(Wbemcli.CIM_REAL32, ByReference::floatValue);
79  		map.put(Wbemcli.CIM_REAL64, ByReference::doubleValue);
80  
81  		map.put(Wbemcli.CIM_CHAR16, ByReference::shortValue);
82  		map.put(Wbemcli.CIM_STRING, ByReference::stringValue);
83  
84  		map.put(Wbemcli.CIM_REFERENCE, WmiCimTypeHandler::convertCimReference);
85  		map.put(Wbemcli.CIM_DATETIME, WmiCimTypeHandler::convertCimDateTime);
86  
87  		CIMTYPE_TO_CONVERTER_MAP = Collections.unmodifiableMap(map);
88  	}
89  
90  	private static final String CIM_OBJECT_LABEL = "CIM_OBJECT";
91  
92  	/**
93  	 * Convert a ByReference value holding a CIM_DATETIME (i.e. a string in the form
94  	 * of <code>yyyymmddHHMMSS.mmmmmmsUUU</code>) to an OffsetDateTime object
95  	 * @param value ByReference value with a CIM_DATETIME string
96  	 * @return OffsetDateTime instance
97  	 */
98  	static OffsetDateTime convertCimDateTime(final ByReference value) {
99  		return Utils.convertCimDateTime(value.stringValue());
100 	}
101 
102 	/**
103 	 * Convert a CIM_REFERENCE into a String
104 	 * @param value CIM_REFERENCE
105 	 * @return a proper String
106 	 */
107 	static String convertCimReference(final ByReference value) {
108 		return WmiCimTypeHandler.convertCimReference(value.stringValue());
109 	}
110 
111 	/**
112 	 * Convert a CIM_REFERENCE into a String
113 	 * @param reference CIM_REFERENCE
114 	 * @return a proper String
115 	 */
116 	static String convertCimReference(final String reference) {
117 		if (reference == null) {
118 			return null;
119 		}
120 
121 		// Remove the "\\hostname\namespace:" prefix (i.e. anything before the first colon)
122 		final int colonIndex = reference.indexOf(':');
123 		return colonIndex > -1 ? reference.substring(colonIndex + 1) : reference;
124 	}
125 
126 	/**
127 	 * Convert the WBEM SAFEARRAY value type into an array.
128 	 *
129 	 * @param array Reference to SAFEARRAY
130 	 * @param property The Property to retrieve. An Entry with the property
131 	 * name as the key and a set of sub properties to retrieve if exists.
132 	 * @return A Map with the property value converted as a Java object, or null if property cannot be retrieved.
133 	 * The key is the property name as defined in the select request.
134 	 * (example: DriveInfo.Name)
135 	 */
136 	static Map<String, Object> convertSafeArray(
137 		final ByReference array,
138 		final int cimType,
139 		final Entry<String, Set<String>> property
140 	) {
141 		// Get the SAFEARRAY
142 		final SAFEARRAY safeArray = (SAFEARRAY) array.getValue();
143 		if (safeArray == null) {
144 			return Collections.singletonMap(property.getKey(), null);
145 		}
146 
147 		safeArray.lock();
148 
149 		// Get the properties of the array
150 		final int lowerBound = safeArray.getLBound(0);
151 		final int length = safeArray.getUBound(0) - lowerBound + 1;
152 
153 		// Convert to a Java array
154 		final Object[] resultArray = new Object[length];
155 		for (int i = 0; i < length; i++) {
156 			resultArray[i] = safeArray.getElement(lowerBound + i);
157 		}
158 
159 		safeArray.unlock();
160 
161 		// Simplified conversion of the values, since SAFEARRAY.getElement()
162 		// did most of the job already, except for CIM_REFERENCE and CIM_DATETIME
163 		if (cimType == Wbemcli.CIM_REFERENCE) {
164 			return Collections.singletonMap(
165 				property.getKey(),
166 				Stream.of(resultArray).map(String.class::cast).map(WmiCimTypeHandler::convertCimReference).toArray()
167 			);
168 		}
169 		if (cimType == Wbemcli.CIM_DATETIME) {
170 			return Collections.singletonMap(
171 				property.getKey(),
172 				Stream.of(resultArray).map(String.class::cast).map(Utils::convertCimDateTime).toArray()
173 			);
174 		}
175 		if (cimType == Wbemcli.CIM_OBJECT) {
176 			if (property.getValue().isEmpty()) {
177 				return Collections.singletonMap(property.getKey(), new String[] { CIM_OBJECT_LABEL });
178 			}
179 
180 			final Map<String, List<Object>> resulMap = new HashMap<>();
181 
182 			for (final Object resultValue : resultArray) {
183 				final Optional<IWbemClassObject> maybeClassObject = getUnknownWbemClassObject(resultValue);
184 				if (!maybeClassObject.isPresent()) {
185 					continue;
186 				}
187 
188 				final Map<String, String> subPropertiesNames = getSubPropertiesNamesFromClass(maybeClassObject.get());
189 
190 				try {
191 					property
192 						.getValue()
193 						.stream()
194 						.map(subProperty -> subPropertiesNames.get(subProperty.toLowerCase()))
195 						.forEach(subProperty ->
196 							resulMap
197 								.computeIfAbsent(buildCimObjectSubPropertyName(property, subProperty), key -> new ArrayList<>())
198 								.add(
199 									getPropertyValue(
200 										maybeClassObject.get(),
201 										new AbstractMap.SimpleEntry<String, Set<String>>(subProperty, Collections.emptySet())
202 									)
203 										.get(subProperty)
204 								)
205 						);
206 				} finally {
207 					maybeClassObject.get().Release();
208 				}
209 			}
210 
211 			return resulMap.entrySet().stream().collect(Collectors.toMap(Entry::getKey, entry -> entry.getValue().toArray()));
212 		}
213 
214 		// Default: return the array straight away
215 		return Collections.singletonMap(property.getKey(), resultArray);
216 	}
217 
218 	/**
219 	 * Convert the wanted values in the CIM Object structure into a Map.
220 	 *
221 	 * @param value The value of the property.
222 	 * @param property The CIM Object Properties to retrieve.
223 	 * An Entry with the CIM Object Class name as the key and a set of sub properties.
224 	 * @return A Map with the properties from the CIM Object and their Values,
225 	 * converted as a Java object, or null if property cannot be retrieved.
226 	 * The key is the property as defined in the select request.
227 	 * (example: DriveInfo.Name)
228 	 */
229 	static Map<String, Object> convertCimObject(final ByReference value, final Entry<String, Set<String>> property) {
230 		final Optional<IWbemClassObject> maybeClassObject = getUnknownWbemClassObject(value.getValue());
231 		if (!maybeClassObject.isPresent()) {
232 			return property
233 				.getValue()
234 				.stream()
235 				.collect(
236 					HashMap::new,
237 					(map, subProperty) -> map.put(buildCimObjectSubPropertyName(property, subProperty), null),
238 					HashMap::putAll
239 				);
240 		}
241 
242 		try {
243 			final Map<String, String> subPropertiesNames = getSubPropertiesNamesFromClass(maybeClassObject.get());
244 
245 			return property
246 				.getValue()
247 				.stream()
248 				.map(subProperty -> subPropertiesNames.get(subProperty.toLowerCase()))
249 				.collect(
250 					HashMap::new,
251 					(map, subProperty) ->
252 						map.put(
253 							buildCimObjectSubPropertyName(property, subProperty),
254 							getPropertyValue(
255 								maybeClassObject.get(),
256 								new AbstractMap.SimpleEntry<String, Set<String>>(subProperty, Collections.emptySet())
257 							)
258 								.get(subProperty)
259 						),
260 					HashMap::putAll
261 				);
262 		} finally {
263 			maybeClassObject.get().Release();
264 		}
265 	}
266 
267 	/**
268 	 * Get a Map of all subProperties names from the class
269 	 * @param wbemClassObject
270 	 * @return
271 	 */
272 	static Map<String, String> getSubPropertiesNamesFromClass(final IWbemClassObject wbemClassObject) {
273 		String[] names;
274 		try {
275 			names = wbemClassObject.GetNames(null, 0, null);
276 		} catch (final Throwable e) {
277 			names = wbemClassObject.GetNames(null, 0, null);
278 		}
279 
280 		return Stream.of(names).collect(Collectors.toMap(String::toLowerCase, Function.identity()));
281 	}
282 
283 	/**
284 	 * Build the CIM Object sub property name as it was in the WQL query.
285 	 * For example: "CimObjectClass.subProperty"
286 	 *
287 	 * @param property The Property to retrieve. An Entry with the property name as the key and a set of sub properties to retrieve if exists.
288 	 * @param subProperty The Sub property name in the CIM Object structure.
289 	 * @return
290 	 */
291 	static String buildCimObjectSubPropertyName(final Entry<String, Set<String>> property, final String subProperty) {
292 		return new StringBuilder().append(property.getKey()).append(".").append(subProperty).toString();
293 	}
294 
295 	/**
296 	 * Get the WbemClassObject representing the CIM Object class from the Unknown Object request value.
297 	 *
298 	 * @param value The value of the property.
299 	 * @return An optional with the WbemClassObject representing the CIM Object class. empty optional otherwise.
300 	 */
301 	static Optional<IWbemClassObject> getUnknownWbemClassObject(final Object value) {
302 		if (value == null) {
303 			return Optional.empty();
304 		}
305 
306 		final Unknown unknown = (Unknown) value;
307 		try {
308 			final PointerByReference pointerByReference = new PointerByReference();
309 
310 			final HRESULT hResult = unknown.QueryInterface(new REFIID(IUnknown.IID_IUNKNOWN), pointerByReference);
311 			return COMUtils.FAILED(hResult)
312 				? Optional.empty()
313 				: Optional.of(new IWbemClassObject(pointerByReference.getValue()));
314 		} finally {
315 			unknown.Release();
316 		}
317 	}
318 
319 	/**
320 	 * Convert the specified CIM value into a Java Object depending on its CIM type.
321 	 * @param value CIM value (ByReference)
322 	 * @param cimType CIM Type
323 	 * @param property The Property to retrieve. An Entry with the property name
324 	 * as the key and a set of sub properties to retrieve if exists.
325 	 * @return A Map with the property value converted as a Java object,
326 	 * or null if property cannot be retrieved.
327 	 * The key is the property name as defined in the select request.
328 	 * (example: DriveInfo.Name)
329 	 */
330 	static Map<String, Object> convert(
331 		final ByReference value,
332 		final int cimType,
333 		final Entry<String, Set<String>> property
334 	) {
335 		if (value.getValue() == null) {
336 			return Collections.singletonMap(property.getKey(), null);
337 		}
338 
339 		// Array?
340 		if ((cimType & Wbemcli.CIM_FLAG_ARRAY) > 0) {
341 			return convertSafeArray(value, cimType ^ Wbemcli.CIM_FLAG_ARRAY, property);
342 		}
343 
344 		if (cimType == Wbemcli.CIM_OBJECT) {
345 			return property.getValue().isEmpty()
346 				? Collections.singletonMap(property.getKey(), CIM_OBJECT_LABEL)
347 				: convertCimObject(value, property);
348 		}
349 
350 		return Collections.singletonMap(
351 			property.getKey(),
352 			CIMTYPE_TO_CONVERTER_MAP.getOrDefault(cimType, v -> "Unsupported type").apply(value)
353 		);
354 	}
355 
356 	/**
357 	 * Get the value of the specified property from the specified WbemClassObject
358 	 * @see <a href="https://docs.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-get">IWbemClassObject::Get method (wbemcli.h)</a>
359 	 * @param wbemClassObject WbemClassObject
360 	 * @param property The Property to retrieve. An Entry with the property name as
361 	 * the key and a set of sub properties to retrieve if exists.
362 	 * @return A Map with the property value converted as a Java object,
363 	 * or null if property cannot be retrieved.
364 	 * The key is the property name as defined in the select request.
365 	 * (example: DriveInfo.Name)
366 	 */
367 	public static Map<String, Object> getPropertyValue(
368 		final IWbemClassObject wbemClassObject,
369 		final Entry<String, Set<String>> property
370 	) {
371 		try {
372 			return getPropertyValueFromWbemObject(wbemClassObject, property);
373 		} catch (final Throwable e) {
374 			// Retry
375 			return getPropertyValueFromWbemObject(wbemClassObject, property);
376 		}
377 	}
378 
379 	private static Map<String, Object> getPropertyValueFromWbemObject(
380 		final IWbemClassObject wbemClassObject,
381 		final Entry<String, Set<String>> property
382 	) {
383 		final ByReference value = new ByReference();
384 		final IntByReference pType = new IntByReference();
385 
386 		// Initialize value, to make sure *VariantClear()* won't throw an exception
387 		// See https://docs.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-get
388 		OleAuto.INSTANCE.VariantInit(value);
389 
390 		try {
391 			final HRESULT hResult = wbemClassObject.Get(property.getKey(), 0, value, pType, new IntByReference());
392 			if (COMUtils.FAILED(hResult)) {
393 				return Collections.singletonMap(property.getKey(), null);
394 			}
395 
396 			// Special case for __PATH
397 			if ("__PATH".equalsIgnoreCase(property.getKey())) {
398 				return Collections.singletonMap(property.getKey(), convertCimReference(value));
399 			}
400 
401 			return convert(value, pType.getValue(), property);
402 		} finally {
403 			try {
404 				OleAuto.INSTANCE.VariantClear(value);
405 			} catch (final Throwable t) {
406 				/* Do nothing -- This condition rarely happens, but it does, and there's nothing we can do about it */
407 			}
408 		}
409 	}
410 }