package au.net.causal.projo.prefs.windows;
import java.lang.annotation.Annotation;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.prefs.Preferences;
import org.apache.commons.lang3.StringUtils;
import au.net.causal.projo.prefs.AbstractPreferenceNode;
import au.net.causal.projo.prefs.PreferenceKeyMetadata;
import au.net.causal.projo.prefs.PreferenceNode;
import au.net.causal.projo.prefs.PreferencesException;
import au.net.causal.projo.prefs.ProjoStore;
import au.net.causal.projo.prefs.UnsupportedDataTypeException;
import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.TypeToken;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.platform.win32.Advapi32;
import com.sun.jna.platform.win32.Advapi32Util;
import com.sun.jna.platform.win32.W32Errors;
import com.sun.jna.platform.win32.Win32Exception;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.platform.win32.WinReg;
import com.sun.jna.platform.win32.WinReg.HKEY;
import com.sun.jna.ptr.IntByReference;
public class WindowsRegistryNode extends AbstractPreferenceNode
{
private static final TypeToken<List<String>> LIST_OF_STRING_TYPE = new TypeToken<List<String>>(){};
private static final Set<TypeToken<?>> SUPPORTED_DATA_TYPES = ImmutableSet.<TypeToken<?>>of(TypeToken.of(String.class),
TypeToken.of(Integer.class),
TypeToken.of(Long.class),
TypeToken.of(byte[].class),
LIST_OF_STRING_TYPE);
private static final Charset CONVERTER_CHARSET = StandardCharsets.ISO_8859_1; //Windows standard
private HKEY root;
/**
* In Windows terminology, a key corresponds to a preference node. A preference key actually corresponds to a Windows registry value.
* Terminology can be a bit confusing.
*/
private String winRegKey;
/**
* Returns the standard location for the root of application settings. It is expected that applications will choose an appropriate
* child node (which {@link ProjoStore} will do by default).
* <p>
*
* Projo chooses <code>HKEY_CURRENT_USER\Software\JavaSoft\projo</code> as the root. It shouldn't go in the Prefs node which Java uses for its
* {@link Preferences} since key names are encoded slightly different. Applications may, however, choose a different node themselves (e.g.
* Software/<application name>) if they wish by using the {@link #WindowsRegistryNode(HKEY, String)} constructor.
*
* @return the root Windows registry node for application settings.
*
* @throws PreferencesException if an error occurs reading the Windows registry.
*/
public static WindowsRegistryNode javaPreferencesRootNode()
throws PreferencesException
{
return(new WindowsRegistryNode(WinReg.HKEY_CURRENT_USER, "Software\\JavaSoft\\projo"));
}
public WindowsRegistryNode(HKEY root, String winRegKey)
{
super(Collections.<Class<? extends Annotation>>emptySet());
if (root == null)
throw new NullPointerException("root == null");
if (winRegKey == null)
throw new NullPointerException("winRegKey == null");
this.root = root;
this.winRegKey = winRegKey;
}
@Override
protected boolean isDataTypeSupportedImpl(PreferenceKeyMetadata<?> keyType) throws PreferencesException
{
return(SUPPORTED_DATA_TYPES.contains(keyType.getDataType()));
}
@Override
protected <T> T getValueImpl(String key, PreferenceKeyMetadata<T> metadata) throws UnsupportedDataTypeException, PreferencesException
{
TypeToken<?> dataType = metadata.getDataType();
try
{
Object regValue = registryGetValue(root, winRegKey, key);
if (TypeToken.of(String.class).equals(dataType))
return((T)valueToString(regValue));
else if (TypeToken.of(Integer.class).equals(dataType))
return((T)valueToInteger(regValue));
else if (TypeToken.of(Long.class).equals(dataType))
return((T)valueToLong(regValue));
else if (TypeToken.of(byte[].class).equals(dataType))
return((T)valueToBytes(regValue));
else if (LIST_OF_STRING_TYPE.equals(dataType))
return((T)valueToStringList(regValue));
else
throw new UnsupportedDataTypeException(metadata.getDataType());
}
catch (Win32Exception e)
{
throw new PreferencesException(e.getMessage(), e);
}
}
private String valueToString(Object regValue)
{
if (regValue == null)
return(null);
else if (regValue instanceof String[])
return(StringUtils.join((String[])regValue, System.lineSeparator())); //just like regedit does it
else if (regValue instanceof byte[])
return(new String((byte[])regValue, CONVERTER_CHARSET));
else //All the rest toString() is fine
return(regValue.toString());
}
private Integer valueToInteger(Object regValue)
{
if (regValue == null)
return(null);
else if (regValue instanceof Number)
return(((Number)regValue).intValue());
else if (regValue instanceof String[])
{
String[] list = (String[])regValue;
if (list.length == 1)
return(valueToInteger(list[0]));
else
return(null); //Cannot convert if zero length or multiple elements
}
else if (regValue instanceof String)
{
//Attempt to parse
try
{
return(Integer.parseInt((String)regValue));
}
catch (NumberFormatException e)
{
//Failed to parse so cannot convert
return(null);
}
}
else if (regValue instanceof byte[])
{
byte[] bValue = (byte[])regValue;
if (bValue.length == Integer.SIZE)
return(ByteBuffer.wrap(bValue).order(ByteOrder.LITTLE_ENDIAN).getInt());
else if (bValue.length == Long.SIZE)
return((int)ByteBuffer.wrap(bValue).order(ByteOrder.LITTLE_ENDIAN).getLong());
else
return(null);
}
else
return(null); //Cannot convert
}
private Long valueToLong(Object regValue)
{
if (regValue == null)
return(null);
else if (regValue instanceof Number)
return(((Number)regValue).longValue());
else if (regValue instanceof String[])
{
String[] list = (String[])regValue;
if (list.length == 1)
return(valueToLong(list[0]));
else
return(null); //Cannot convert if zero length or multiple elements
}
else if (regValue instanceof String)
{
//Attempt to parse
try
{
return(Long.parseLong((String)regValue));
}
catch (NumberFormatException e)
{
//Failed to parse so cannot convert
return(null);
}
}
else if (regValue instanceof byte[])
{
byte[] bValue = (byte[])regValue;
if (bValue.length == Integer.SIZE)
return((long)ByteBuffer.wrap(bValue).order(ByteOrder.LITTLE_ENDIAN).getInt());
else if (bValue.length == Long.SIZE)
return(ByteBuffer.wrap(bValue).order(ByteOrder.LITTLE_ENDIAN).getLong());
else
return(null);
}
else
return(null); //Cannot convert
}
private byte[] valueToBytes(Object regValue)
{
if (regValue == null)
return(null);
else if (regValue instanceof byte[])
return((byte[])regValue);
else if (regValue instanceof String[])
return(valueToBytes(valueToString(regValue)));
else if (regValue instanceof String)
return(((String)regValue).getBytes(CONVERTER_CHARSET));
else if (regValue instanceof Integer)
return(ByteBuffer.allocate(Integer.SIZE / 8).order(ByteOrder.LITTLE_ENDIAN).putInt((Integer)regValue).array());
else if (regValue instanceof Long)
return(ByteBuffer.allocate(Long.SIZE / 8).order(ByteOrder.LITTLE_ENDIAN).putLong((Long)regValue).array());
else
return(null); //Cannot convert
}
private List<String> valueToStringList(Object regValue)
{
if (regValue == null)
return(null);
else if (regValue instanceof String[])
return(Arrays.asList((String[])regValue));
else
{
String s = valueToString(regValue);
if (s == null)
return(null);
else
return(Collections.singletonList(s));
}
}
@Override
protected <T> void putValueImpl(String key, T value, PreferenceKeyMetadata<T> metadata) throws UnsupportedDataTypeException, PreferencesException
{
TypeToken<T> dataType = metadata.getDataType();
//Null values mean remove the value from the registry
if (value == null)
{
removeValue(key, metadata);
return;
}
if (!existsInRegistry())
RegistryUtils.createKeyPath(root, winRegKey);
try
{
if (TypeToken.of(String.class).equals(dataType))
Advapi32Util.registrySetStringValue(root, winRegKey, key, (String)value);
else if (TypeToken.of(Integer.class).equals(dataType))
Advapi32Util.registrySetIntValue(root, winRegKey, key, (Integer)value);
else if (TypeToken.of(Long.class).equals(dataType))
Advapi32Util.registrySetLongValue(root, winRegKey, key, (Long)value);
else if (LIST_OF_STRING_TYPE.equals(dataType))
{
@SuppressWarnings("unchecked") //Safe
List<String> list = (List<String>)value;
String[] arrayValue = list.toArray(new String[list.size()]);
Advapi32Util.registrySetStringArray(root, winRegKey, key, arrayValue);
}
else if (TypeToken.of(byte[].class).equals(dataType))
Advapi32Util.registrySetBinaryValue(root, winRegKey, key, (byte[])value);
else
throw new UnsupportedDataTypeException(metadata.getDataType());
}
catch (Win32Exception e)
{
throw new PreferencesException(e.getMessage(), e);
}
}
@Override
protected <T> void removeValueImpl(String key, PreferenceKeyMetadata<T> metadata) throws UnsupportedDataTypeException, PreferencesException
{
//No removal needed if the whole registry key does not exist
if (!existsInRegistry())
return;
try
{
Advapi32Util.registryDeleteValue(root, winRegKey, key);
}
catch (Win32Exception e)
{
throw new PreferencesException(e.getMessage(), e);
}
}
@Override
public void removeAllValues() throws PreferencesException
{
//No removal needed if the whole registry key does not exist
if (!existsInRegistry())
return;
for (String keyName : getKeyNames())
{
try
{
Advapi32Util.registryDeleteValue(root, winRegKey, keyName);
}
catch (Win32Exception e)
{
throw new PreferencesException(e.getMessage(), e);
}
}
}
@Override
public PreferenceNode getChildNode(String name) throws PreferencesException
{
String childRegKey = winRegKey + RegistryUtils.NODE_SEPARATOR + name;
return(new WindowsRegistryNode(root, childRegKey));
}
@Override
public void removeChildNode(String name) throws PreferencesException
{
try
{
RegistryUtils.deleteKeyWithChildren(root, winRegKey + RegistryUtils.NODE_SEPARATOR + name);
}
catch (Win32Exception e)
{
throw new PreferencesException(e.getMessage(), e);
}
}
@Override
public Set<String> getKeyNames() throws PreferencesException
{
if (!existsInRegistry())
return(Collections.emptySet());
try
{
return(Advapi32Util.registryGetValues(root, winRegKey).keySet());
}
catch (Win32Exception e)
{
throw new PreferencesException(e.getMessage(), e);
}
}
/**
* Returns true if this node exists in the Windows registry.
*
* @return true if exists, false if not.
*
* @throws PreferencesException if an error occurs.
*/
private boolean existsInRegistry()
throws PreferencesException
{
try
{
return(Advapi32Util.registryKeyExists(root, winRegKey));
}
catch (Win32Exception e)
{
throw new PreferencesException(e.getMessage(), e);
}
}
@Override
public Set<String> getNodeNames() throws PreferencesException
{
if (!existsInRegistry())
return(Collections.emptySet());
try
{
return(ImmutableSet.<String>builder().addAll(Arrays.asList(Advapi32Util.registryGetKeys(root, winRegKey))).build());
}
catch (Win32Exception e)
{
throw new PreferencesException(e.getMessage(), e);
}
}
/**
* Get a registry value and returns a java object depending on the value
* type.
* <p>
*
* This was mostly copied from {@link Advapi32Util} class but adds support for REG_MULTI_SZ type which was apparently lacking in the original.
*
* @param hkKey
* Root key.
* @param subKey
* Registry key path.
* @param lpValueName
* Name of the value to retrieve or null for the default value.
* @return Object value.
*/
private static Object registryGetValue(HKEY hkKey, String subKey,
String lpValueName) {
Object result = null;
IntByReference lpType = new IntByReference();
byte[] lpData = new byte[Advapi32.MAX_VALUE_NAME];
IntByReference lpcbData = new IntByReference(Advapi32.MAX_VALUE_NAME);
int rc = Advapi32.INSTANCE.RegGetValue(hkKey, subKey, lpValueName,
Advapi32.RRF_RT_ANY, lpType, lpData, lpcbData);
// if lpType == 0 then the value is empty (REG_NONE)!
if (lpType.getValue() == WinNT.REG_NONE)
return null;
if (rc != W32Errors.ERROR_SUCCESS
&& rc != W32Errors.ERROR_INSUFFICIENT_BUFFER) {
throw new Win32Exception(rc);
}
Memory byteData = new Memory(lpcbData.getValue());
byteData.write(0, lpData, 0, lpcbData.getValue());
if (lpType.getValue() == WinNT.REG_DWORD) {
result = new Integer(byteData.getInt(0));
} else if (lpType.getValue() == WinNT.REG_QWORD) {
result = new Long(byteData.getLong(0));
} else if (lpType.getValue() == WinNT.REG_BINARY) {
result = byteData.getByteArray(0, lpcbData.getValue());
} else if ((lpType.getValue() == WinNT.REG_SZ)
|| (lpType.getValue() == WinNT.REG_EXPAND_SZ)) {
result = byteData.getWideString(0);
} else if ((lpType.getValue() == WinNT.REG_MULTI_SZ)) {
ArrayList<String> resultList = new ArrayList<>();
int offset = 0;
while (offset < byteData.size()) {
String s = byteData.getWideString(offset);
offset += s.length() * Native.WCHAR_SIZE;
offset += Native.WCHAR_SIZE;
if (s.length() == 0 && offset == byteData.size()) {
// skip the final NULL
} else {
resultList.add(s);
}
}
result = resultList.toArray(new String[resultList.size()]);
}
return result;
}
@Override
public void flush() throws PreferencesException
{
//Everything is immediately flushed anyway so nothing to do here
}
@Override
public void close() throws PreferencesException
{
flush();
}
}