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, WmiCimTypeHandler::convertCimUint32);
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_UINT32 (i.e. an unsigned int on 32 bits).
94  	 * <p>
95  	 * Unfortunately, there seems to be a bug in either JNA or Wbemcli library which stores
96  	 * CIM_UINT32 values as VT_I4 (signed integer). As a result, it's converted to a signed
97  	 * integer, which is wrong.
98  	 * <p>
99  	 * Good news, since we know the CIM type, we can enforce reconverting to unsigned!
100 	 * <p>
101 	 * @param cimUint32Value a CIM_UINT32 value... potentially (and wrongly) stored as a VT_I4
102 	 * @return always a positive long
103 	 */
104 	private static long convertCimUint32(final ByReference cimUint32Value) {
105 		return Integer.toUnsignedLong(cimUint32Value.intValue());
106 	}
107 
108 	/**
109 	 * Convert a ByReference value holding a CIM_DATETIME (i.e. a string in the form
110 	 * of <code>yyyymmddHHMMSS.mmmmmmsUUU</code>) to an OffsetDateTime object
111 	 * @param value ByReference value with a CIM_DATETIME string
112 	 * @return OffsetDateTime instance
113 	 */
114 	static OffsetDateTime convertCimDateTime(final ByReference value) {
115 		return Utils.convertCimDateTime(value.stringValue());
116 	}
117 
118 	/**
119 	 * Convert a CIM_REFERENCE into a String
120 	 * @param value CIM_REFERENCE
121 	 * @return a proper String
122 	 */
123 	static String convertCimReference(final ByReference value) {
124 		return WmiCimTypeHandler.convertCimReference(value.stringValue());
125 	}
126 
127 	/**
128 	 * Convert a CIM_REFERENCE into a String
129 	 * @param reference CIM_REFERENCE
130 	 * @return a proper String
131 	 */
132 	static String convertCimReference(final String reference) {
133 		if (reference == null) {
134 			return null;
135 		}
136 
137 		// Remove the "\\hostname\namespace:" prefix (i.e. anything before the first colon)
138 		final int colonIndex = reference.indexOf(':');
139 		return colonIndex > -1 ? reference.substring(colonIndex + 1) : reference;
140 	}
141 
142 	/**
143 	 * Convert the WBEM SAFEARRAY value type into an array.
144 	 *
145 	 * @param array Reference to SAFEARRAY
146 	 * @param property The Property to retrieve. An Entry with the property
147 	 * name as the key and a set of sub properties to retrieve if exists.
148 	 * @return A Map with the property value converted as a Java object, or null if property cannot be retrieved.
149 	 * The key is the property name as defined in the select request.
150 	 * (example: DriveInfo.Name)
151 	 */
152 	static Map<String, Object> convertSafeArray(
153 		final ByReference array,
154 		final int cimType,
155 		final Entry<String, Set<String>> property
156 	) {
157 		// Get the SAFEARRAY
158 		final SAFEARRAY safeArray = (SAFEARRAY) array.getValue();
159 		if (safeArray == null) {
160 			return Collections.singletonMap(property.getKey(), null);
161 		}
162 
163 		safeArray.lock();
164 
165 		// Get the properties of the array
166 		final int lowerBound = safeArray.getLBound(0);
167 		final int length = safeArray.getUBound(0) - lowerBound + 1;
168 
169 		// Convert to a Java array
170 		final Object[] resultArray = new Object[length];
171 		for (int i = 0; i < length; i++) {
172 			resultArray[i] = safeArray.getElement(lowerBound + i);
173 		}
174 
175 		safeArray.unlock();
176 
177 		// Simplified conversion of the values, since SAFEARRAY.getElement()
178 		// did most of the job already, except for CIM_REFERENCE and CIM_DATETIME
179 		if (cimType == Wbemcli.CIM_REFERENCE) {
180 			return Collections.singletonMap(
181 				property.getKey(),
182 				Stream.of(resultArray).map(String.class::cast).map(WmiCimTypeHandler::convertCimReference).toArray()
183 			);
184 		}
185 		if (cimType == Wbemcli.CIM_DATETIME) {
186 			return Collections.singletonMap(
187 				property.getKey(),
188 				Stream.of(resultArray).map(String.class::cast).map(Utils::convertCimDateTime).toArray()
189 			);
190 		}
191 		if (cimType == Wbemcli.CIM_OBJECT) {
192 			if (property.getValue().isEmpty()) {
193 				return Collections.singletonMap(property.getKey(), new String[] { CIM_OBJECT_LABEL });
194 			}
195 
196 			final Map<String, List<Object>> resulMap = new HashMap<>();
197 
198 			for (final Object resultValue : resultArray) {
199 				final Optional<IWbemClassObject> maybeClassObject = getUnknownWbemClassObject(resultValue);
200 				if (!maybeClassObject.isPresent()) {
201 					continue;
202 				}
203 
204 				final Map<String, String> subPropertiesNames = getSubPropertiesNamesFromClass(maybeClassObject.get());
205 
206 				try {
207 					property
208 						.getValue()
209 						.stream()
210 						.map(subProperty -> subPropertiesNames.get(subProperty.toLowerCase()))
211 						.forEach(subProperty ->
212 							resulMap
213 								.computeIfAbsent(buildCimObjectSubPropertyName(property, subProperty), key -> new ArrayList<>())
214 								.add(
215 									getPropertyValue(
216 										maybeClassObject.get(),
217 										new AbstractMap.SimpleEntry<String, Set<String>>(subProperty, Collections.emptySet())
218 									)
219 										.get(subProperty)
220 								)
221 						);
222 				} finally {
223 					maybeClassObject.get().Release();
224 				}
225 			}
226 
227 			return resulMap.entrySet().stream().collect(Collectors.toMap(Entry::getKey, entry -> entry.getValue().toArray()));
228 		}
229 
230 		// Default: return the array straight away
231 		return Collections.singletonMap(property.getKey(), resultArray);
232 	}
233 
234 	/**
235 	 * Convert the wanted values in the CIM Object structure into a Map.
236 	 *
237 	 * @param value The value of the property.
238 	 * @param property The CIM Object Properties to retrieve.
239 	 * An Entry with the CIM Object Class name as the key and a set of sub properties.
240 	 * @return A Map with the properties from the CIM Object and their Values,
241 	 * converted as a Java object, or null if property cannot be retrieved.
242 	 * The key is the property as defined in the select request.
243 	 * (example: DriveInfo.Name)
244 	 */
245 	static Map<String, Object> convertCimObject(final ByReference value, final Entry<String, Set<String>> property) {
246 		final Optional<IWbemClassObject> maybeClassObject = getUnknownWbemClassObject(value.getValue());
247 		if (!maybeClassObject.isPresent()) {
248 			return property
249 				.getValue()
250 				.stream()
251 				.collect(
252 					HashMap::new,
253 					(map, subProperty) -> map.put(buildCimObjectSubPropertyName(property, subProperty), null),
254 					HashMap::putAll
255 				);
256 		}
257 
258 		try {
259 			final Map<String, String> subPropertiesNames = getSubPropertiesNamesFromClass(maybeClassObject.get());
260 
261 			return property
262 				.getValue()
263 				.stream()
264 				.map(subProperty -> subPropertiesNames.get(subProperty.toLowerCase()))
265 				.collect(
266 					HashMap::new,
267 					(map, subProperty) ->
268 						map.put(
269 							buildCimObjectSubPropertyName(property, subProperty),
270 							getPropertyValue(
271 								maybeClassObject.get(),
272 								new AbstractMap.SimpleEntry<String, Set<String>>(subProperty, Collections.emptySet())
273 							)
274 								.get(subProperty)
275 						),
276 					HashMap::putAll
277 				);
278 		} finally {
279 			maybeClassObject.get().Release();
280 		}
281 	}
282 
283 	/**
284 	 * Get a Map of all subProperties names from the class
285 	 * @param wbemClassObject
286 	 * @return
287 	 */
288 	static Map<String, String> getSubPropertiesNamesFromClass(final IWbemClassObject wbemClassObject) {
289 		String[] names;
290 		try {
291 			names = wbemClassObject.GetNames(null, 0, null);
292 		} catch (final Throwable e) {
293 			names = wbemClassObject.GetNames(null, 0, null);
294 		}
295 
296 		return Stream.of(names).collect(Collectors.toMap(String::toLowerCase, Function.identity()));
297 	}
298 
299 	/**
300 	 * Build the CIM Object sub property name as it was in the WQL query.
301 	 * For example: "CimObjectClass.subProperty"
302 	 *
303 	 * @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.
304 	 * @param subProperty The Sub property name in the CIM Object structure.
305 	 * @return
306 	 */
307 	static String buildCimObjectSubPropertyName(final Entry<String, Set<String>> property, final String subProperty) {
308 		return new StringBuilder().append(property.getKey()).append(".").append(subProperty).toString();
309 	}
310 
311 	/**
312 	 * Get the WbemClassObject representing the CIM Object class from the Unknown Object request value.
313 	 *
314 	 * @param value The value of the property.
315 	 * @return An optional with the WbemClassObject representing the CIM Object class. empty optional otherwise.
316 	 */
317 	static Optional<IWbemClassObject> getUnknownWbemClassObject(final Object value) {
318 		if (value == null) {
319 			return Optional.empty();
320 		}
321 
322 		final Unknown unknown = (Unknown) value;
323 		try {
324 			final PointerByReference pointerByReference = new PointerByReference();
325 
326 			final HRESULT hResult = unknown.QueryInterface(new REFIID(IUnknown.IID_IUNKNOWN), pointerByReference);
327 			return COMUtils.FAILED(hResult)
328 				? Optional.empty()
329 				: Optional.of(new IWbemClassObject(pointerByReference.getValue()));
330 		} finally {
331 			unknown.Release();
332 		}
333 	}
334 
335 	/**
336 	 * Convert the specified CIM value into a Java Object depending on its CIM type.
337 	 * @param value CIM value (ByReference)
338 	 * @param cimType CIM Type
339 	 * @param property The Property to retrieve. An Entry with the property name
340 	 * as the key and a set of sub properties to retrieve if exists.
341 	 * @return A Map with the property value converted as a Java object,
342 	 * or null if property cannot be retrieved.
343 	 * The key is the property name as defined in the select request.
344 	 * (example: DriveInfo.Name)
345 	 */
346 	static Map<String, Object> convert(
347 		final ByReference value,
348 		final int cimType,
349 		final Entry<String, Set<String>> property
350 	) {
351 		if (value.getValue() == null) {
352 			return Collections.singletonMap(property.getKey(), null);
353 		}
354 
355 		// Array?
356 		if ((cimType & Wbemcli.CIM_FLAG_ARRAY) > 0) {
357 			return convertSafeArray(value, cimType ^ Wbemcli.CIM_FLAG_ARRAY, property);
358 		}
359 
360 		if (cimType == Wbemcli.CIM_OBJECT) {
361 			return property.getValue().isEmpty()
362 				? Collections.singletonMap(property.getKey(), CIM_OBJECT_LABEL)
363 				: convertCimObject(value, property);
364 		}
365 
366 		return Collections.singletonMap(
367 			property.getKey(),
368 			CIMTYPE_TO_CONVERTER_MAP.getOrDefault(cimType, v -> "Unsupported type").apply(value)
369 		);
370 	}
371 
372 	/**
373 	 * Get the value of the specified property from the specified WbemClassObject
374 	 * @see <a href="https://docs.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-get">IWbemClassObject::Get method (wbemcli.h)</a>
375 	 * @param wbemClassObject WbemClassObject
376 	 * @param property The Property to retrieve. An Entry with the property name as
377 	 * the key and a set of sub properties to retrieve if exists.
378 	 * @return A Map with the property value converted as a Java object,
379 	 * or null if property cannot be retrieved.
380 	 * The key is the property name as defined in the select request.
381 	 * (example: DriveInfo.Name)
382 	 */
383 	public static Map<String, Object> getPropertyValue(
384 		final IWbemClassObject wbemClassObject,
385 		final Entry<String, Set<String>> property
386 	) {
387 		try {
388 			return getPropertyValueFromWbemObject(wbemClassObject, property);
389 		} catch (final Throwable e) {
390 			// Retry
391 			return getPropertyValueFromWbemObject(wbemClassObject, property);
392 		}
393 	}
394 
395 	private static Map<String, Object> getPropertyValueFromWbemObject(
396 		final IWbemClassObject wbemClassObject,
397 		final Entry<String, Set<String>> property
398 	) {
399 		final ByReference value = new ByReference();
400 		final IntByReference pType = new IntByReference();
401 
402 		// Initialize value, to make sure *VariantClear()* won't throw an exception
403 		// See https://docs.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-get
404 		OleAuto.INSTANCE.VariantInit(value);
405 
406 		try {
407 			final HRESULT hResult = wbemClassObject.Get(property.getKey(), 0, value, pType, new IntByReference());
408 			if (COMUtils.FAILED(hResult)) {
409 				return Collections.singletonMap(property.getKey(), null);
410 			}
411 
412 			// Special case for __PATH
413 			if ("__PATH".equalsIgnoreCase(property.getKey())) {
414 				return Collections.singletonMap(property.getKey(), convertCimReference(value));
415 			}
416 
417 			return convert(value, pType.getValue(), property);
418 		} finally {
419 			try {
420 				OleAuto.INSTANCE.VariantClear(value);
421 			} catch (final Throwable t) {
422 				/* Do nothing -- This condition rarely happens, but it does, and there's nothing we can do about it */
423 			}
424 		}
425 	}
426 }