All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sun.jna.platform.win32.COM.util.ProxyObject Maven / Gradle / Ivy

There is a newer version: 1.50.0
Show newest version
/* Copyright (c) 2014 Dr David H. Akehurst (itemis), All Rights Reserved
 *
 * The contents of this file is dual-licensed under 2
 * alternative Open Source/Free licenses: LGPL 2.1 or later and
 * Apache License 2.0. (starting with JNA version 4.0.0).
 *
 * You can freely decide which license you want to apply to
 * the project.
 *
 * You may obtain a copy of the LGPL License at:
 *
 * http://www.gnu.org/licenses/licenses.html
 *
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "LGPL2.1".
 *
 * You may obtain a copy of the Apache License at:
 *
 * http://www.apache.org/licenses/
 *
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "AL2.0".
 */
package com.sun.jna.platform.win32.COM.util;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;

import com.sun.jna.Pointer;
import com.sun.jna.WString;
import com.sun.jna.internal.ReflectionUtils;
import com.sun.jna.platform.win32.Guid;
import com.sun.jna.platform.win32.Guid.IID;
import com.sun.jna.platform.win32.Guid.REFIID;
import com.sun.jna.platform.win32.Kernel32Util;
import com.sun.jna.platform.win32.OaIdl;
import com.sun.jna.platform.win32.OaIdl.DISPID;
import com.sun.jna.platform.win32.OaIdl.DISPIDByReference;
import com.sun.jna.platform.win32.OaIdl.EXCEPINFO;
import com.sun.jna.platform.win32.OleAuto;
import com.sun.jna.platform.win32.OleAuto.DISPPARAMS;
import com.sun.jna.platform.win32.Variant;
import com.sun.jna.platform.win32.Variant.VARIANT;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinDef.DWORDByReference;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.platform.win32.WinNT.HRESULT;
import com.sun.jna.platform.win32.COM.COMException;
import com.sun.jna.platform.win32.COM.COMUtils;
import com.sun.jna.platform.win32.COM.ConnectionPoint;
import com.sun.jna.platform.win32.COM.ConnectionPointContainer;
import com.sun.jna.platform.win32.COM.Dispatch;
import com.sun.jna.platform.win32.COM.IDispatch;
import com.sun.jna.platform.win32.COM.IDispatchCallback;
import com.sun.jna.platform.win32.COM.util.annotation.ComInterface;
import com.sun.jna.platform.win32.COM.util.annotation.ComMethod;
import com.sun.jna.platform.win32.COM.util.annotation.ComProperty;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;

/**
 * This object acts as the invocation handler for interfaces annotated with
 * ComInterface. It wraps all (necessary) low level COM calls and dispatches
 * them through the COM runtime.
 *
 * 

The caller of the methods is responsible for correct initialization of the * COM runtime and appropriate thread-handling - depending on the choosen * handling model.

* * @see MSDN - Processes, Threads, and Apartments * @see MSDN - Understanding and Using COM Threading Models */ public class ProxyObject implements InvocationHandler, com.sun.jna.platform.win32.COM.util.IDispatch, IRawDispatchHandle, IConnectionPoint { // cached value of the IUnknown interface pointer // Rules of COM state that querying for the IUnknown interface must return // an identical pointer value private long unknownId; private final Class theInterface; private final ObjectFactory factory; private final com.sun.jna.platform.win32.COM.IDispatch rawDispatch; @SuppressWarnings("LeakingThisInConstructor") public ProxyObject(Class theInterface, IDispatch rawDispatch, ObjectFactory factory) { this.unknownId = -1; this.rawDispatch = rawDispatch; this.theInterface = theInterface; this.factory = factory; // make sure dispatch object knows we have a reference to it // (for debug it is usefult to be able to see how many refs are present int n = this.rawDispatch.AddRef(); this.getUnknownId(); // pre cache/calculate it factory.register(this); } private long getUnknownId() { assert COMUtils.comIsInitialized() : "COM not initialized"; if (-1 == this.unknownId) { try { final PointerByReference ppvObject = new PointerByReference(); Thread current = Thread.currentThread(); String tn = current.getName(); IID iid = com.sun.jna.platform.win32.COM.IUnknown.IID_IUNKNOWN; HRESULT hr = ProxyObject.this.getRawDispatch().QueryInterface(new REFIID(iid), ppvObject); if (WinNT.S_OK.equals(hr)) { Dispatch dispatch = new Dispatch(ppvObject.getValue()); this.unknownId = Pointer.nativeValue(dispatch.getPointer()); // QueryInterface returns a COM object pointer with a +1 // reference, we must drop one, // Note: createProxy adds one; int n = dispatch.Release(); } else { String formatMessageFromHR = Kernel32Util.formatMessage(hr); throw new COMException("getUnknownId: " + formatMessageFromHR, hr); } } catch (RuntimeException e) { // Do not rewrap COMException if (e instanceof COMException) { throw e; } else { throw new COMException("Error occured when trying get Unknown Id ", e); } } } return this.unknownId; } @Override protected void finalize() throws Throwable { this.dispose(); super.finalize(); } public synchronized void dispose() { if (((Dispatch) this.rawDispatch).getPointer() != Pointer.NULL) { this.rawDispatch.Release(); ((Dispatch) this.rawDispatch).setPointer(Pointer.NULL); factory.unregister(this); } } @Override public com.sun.jna.platform.win32.COM.IDispatch getRawDispatch() { return this.rawDispatch; } // -------------------- Object ------------------------- /* * The QueryInterface rule state that 'a call to QueryInterface with * IID_IUnknown must always return the same physical pointer value.' * * [http://msdn.microsoft.com/en-us/library/ms686590%28VS.85%29.aspx] * * therefore we can compare the pointers */ @Override public boolean equals(Object arg) { if (null == arg) { return false; } else if (arg instanceof ProxyObject) { ProxyObject other = (ProxyObject) arg; return this.getUnknownId() == other.getUnknownId(); } else if (Proxy.isProxyClass(arg.getClass())) { InvocationHandler handler = Proxy.getInvocationHandler(arg); if (handler instanceof ProxyObject) { try { ProxyObject other = (ProxyObject) handler; return this.getUnknownId() == other.getUnknownId(); } catch (Exception e) { // if can't do this comparison, return false // (queryInterface may throw if COM objects become invalid) return false; } } else { return false; } } else { return false; } } @Override public int hashCode() { long id = this.getUnknownId(); return (int) ((id >>> 32) & 0xFFFFFFFF) + (int) (id & 0xFFFFFFFF); } @Override public String toString() { return this.theInterface.getName() + "{unk=" + this.hashCode() + "}"; } // --------------------- InvocationHandler ----------------------------- @Override public Object invoke(final Object proxy, final java.lang.reflect.Method method, final Object[] args) throws Throwable { boolean declaredAsInterface = (method.getAnnotation(ComMethod.class) != null) || (method.getAnnotation(ComProperty.class) != null); if ((!declaredAsInterface) && (method.getDeclaringClass().equals(Object.class) || method.getDeclaringClass().equals(IRawDispatchHandle.class) || method.getDeclaringClass().equals(com.sun.jna.platform.win32.COM.util.IUnknown.class) || method.getDeclaringClass().equals(com.sun.jna.platform.win32.COM.util.IDispatch.class) || method.getDeclaringClass().equals(IConnectionPoint.class))) { try { return method.invoke(this, args); } catch (InvocationTargetException ex) { throw ex.getCause(); } } if((!declaredAsInterface) && ReflectionUtils.isDefault(method)) { Object methodHandle = ReflectionUtils.getMethodHandle(method); return ReflectionUtils.invokeDefaultMethod(proxy, methodHandle, args); } Class returnType = method.getReturnType(); boolean isVoid = Void.TYPE.equals(returnType); ComProperty prop = method.getAnnotation(ComProperty.class); if (null != prop) { int dispId = prop.dispId(); Object[] fullLengthArgs = unfoldWhenVarargs(method, args); if (isVoid) { if (dispId != -1) { this.setProperty(new DISPID(dispId), fullLengthArgs); return null; } else { String propName = this.getMutatorName(method, prop); this.setProperty(propName, fullLengthArgs); return null; } } else { if (dispId != -1) { return this.getProperty(returnType, new DISPID(dispId), args); } else { String propName = this.getAccessorName(method, prop); return this.getProperty(returnType, propName, args); } } } ComMethod meth = method.getAnnotation(ComMethod.class); if (null != meth) { Object[] fullLengthArgs = unfoldWhenVarargs(method, args); int dispId = meth.dispId(); if (dispId != -1) { return this.invokeMethod(returnType, new DISPID(dispId), fullLengthArgs); } else { String methName = this.getMethodName(method, meth); return this.invokeMethod(returnType, methName, fullLengthArgs); } } return null; } // ---------------------- IConnectionPoint ---------------------- private ConnectionPoint fetchRawConnectionPoint(IID iid) { assert COMUtils.comIsInitialized() : "COM not initialized"; // query for ConnectionPointContainer IConnectionPointContainer cpc = this.queryInterface(IConnectionPointContainer.class); Dispatch rawCpcDispatch = (Dispatch) cpc.getRawDispatch(); final ConnectionPointContainer rawCpc = new ConnectionPointContainer(rawCpcDispatch.getPointer()); // find connection point for comEventCallback interface final REFIID adviseRiid = new REFIID(iid.getPointer()); final PointerByReference ppCp = new PointerByReference(); HRESULT hr = rawCpc.FindConnectionPoint(adviseRiid, ppCp); COMUtils.checkRC(hr); final ConnectionPoint rawCp = new ConnectionPoint(ppCp.getValue()); return rawCp; } @Override public IComEventCallbackCookie advise(Class comEventCallbackInterface, final IComEventCallbackListener comEventCallbackListener) throws COMException { assert COMUtils.comIsInitialized() : "COM not initialized"; try { ComInterface comInterfaceAnnotation = comEventCallbackInterface.getAnnotation(ComInterface.class); if (null == comInterfaceAnnotation) { throw new COMException( "advise: Interface must define a value for either iid via the ComInterface annotation"); } final IID iid = this.getIID(comInterfaceAnnotation); final ConnectionPoint rawCp = this.fetchRawConnectionPoint(iid); // create the dispatch listener final IDispatchCallback rawListener = factory.createDispatchCallback(comEventCallbackInterface, comEventCallbackListener); // store it the comEventCallback argument, so it is not garbage // collected. comEventCallbackListener.setDispatchCallbackListener(rawListener); // set the dispatch listener to listen to events from the connection // point final DWORDByReference pdwCookie = new DWORDByReference(); HRESULT hr = rawCp.Advise(rawListener, pdwCookie); int n = rawCp.Release(); // release before check in case check // throws exception COMUtils.checkRC(hr); // return the cookie so that a call to stop listening can be made return new ComEventCallbackCookie(pdwCookie.getValue()); } catch (RuntimeException e) { // Do not rewrap COMException if (e instanceof COMException) { throw e; } else { throw new COMException("Error occured in advise when trying to connect the listener " + comEventCallbackListener, e); } } } @Override public void unadvise(Class comEventCallbackInterface, final IComEventCallbackCookie cookie) throws COMException { assert COMUtils.comIsInitialized() : "COM not initialized"; try { ComInterface comInterfaceAnnotation = comEventCallbackInterface.getAnnotation(ComInterface.class); if (null == comInterfaceAnnotation) { throw new COMException( "unadvise: Interface must define a value for iid via the ComInterface annotation"); } IID iid = this.getIID(comInterfaceAnnotation); final ConnectionPoint rawCp = this.fetchRawConnectionPoint(iid); HRESULT hr = rawCp.Unadvise(((ComEventCallbackCookie) cookie).getValue()); rawCp.Release(); COMUtils.checkRC(hr); } catch (RuntimeException e) { // Do not rewrap COMException if (e instanceof COMException) { throw e; } else { throw new COMException("Error occured in unadvise when trying to disconnect the listener from " + this, e); } } } // --------------------- IDispatch ------------------------------ @Override public void setProperty(String name, T value) { DISPID dispID = resolveDispId(this.getRawDispatch(), name); setProperty(dispID, value); } @Override public void setProperty(DISPID dispId, T value) { assert COMUtils.comIsInitialized() : "COM not initialized"; VARIANT v = Convert.toVariant(value); WinNT.HRESULT hr = this.oleMethod(OleAuto.DISPATCH_PROPERTYPUT, null, this.getRawDispatch(), dispId, v); Convert.free(v, value); // Free value allocated by Convert#toVariant COMUtils.checkRC(hr); } private void setProperty(String name, Object... args) { assert COMUtils.comIsInitialized() : "COM not initialized"; DISPID dispID = resolveDispId(this.getRawDispatch(), name); setProperty(dispID, args); } private void setProperty(DISPID dispID, Object... args) { assert COMUtils.comIsInitialized() : "COM not initialized"; VARIANT[] vargs; if (null == args) { vargs = new VARIANT[0]; } else { vargs = new VARIANT[args.length]; } for (int i = 0; i < vargs.length; ++i) { vargs[i] = Convert.toVariant(args[i]); } Variant.VARIANT.ByReference result = new Variant.VARIANT.ByReference(); WinNT.HRESULT hr = this.oleMethod(OleAuto.DISPATCH_PROPERTYPUT, null, this.getRawDispatch(), dispID, vargs); for (int i = 0; i < vargs.length; i++) { // Free value allocated by Convert#toVariant Convert.free(vargs[i], args[i]); } COMUtils.checkRC(hr); } @Override public T getProperty(Class returnType, String name, Object... args) { DISPID dispID = resolveDispId(this.getRawDispatch(), name); return getProperty(returnType, dispID, args); } @Override public T getProperty(Class returnType, DISPID dispID, Object... args) { VARIANT[] vargs; if (null == args) { vargs = new VARIANT[0]; } else { vargs = new VARIANT[args.length]; } for (int i = 0; i < vargs.length; ++i) { vargs[i] = Convert.toVariant(args[i]); } Variant.VARIANT.ByReference result = new Variant.VARIANT.ByReference(); WinNT.HRESULT hr = this.oleMethod(OleAuto.DISPATCH_PROPERTYGET, result, this.getRawDispatch(), dispID, vargs); for (int i = 0; i < vargs.length; i++) { // Free value allocated by Convert#toVariant Convert.free(vargs[i], args[i]); } COMUtils.checkRC(hr); return (T) Convert.toJavaObject(result, returnType, factory, false, true); } @Override public T invokeMethod(Class returnType, String name, Object... args) { DISPID dispID = resolveDispId(this.getRawDispatch(), name); return invokeMethod(returnType, dispID, args); } @Override public T invokeMethod(Class returnType, DISPID dispID, Object... args) { assert COMUtils.comIsInitialized() : "COM not initialized"; VARIANT[] vargs; if (null == args) { vargs = new VARIANT[0]; } else { vargs = new VARIANT[args.length]; } for (int i = 0; i < vargs.length; ++i) { vargs[i] = Convert.toVariant(args[i]); } Variant.VARIANT.ByReference result = new Variant.VARIANT.ByReference(); WinNT.HRESULT hr = this.oleMethod(OleAuto.DISPATCH_METHOD, result, this.getRawDispatch(), dispID, vargs); for (int i = 0; i < vargs.length; i++) { // Free value allocated by Convert#toVariant Convert.free(vargs[i], args[i]); } COMUtils.checkRC(hr); return (T) Convert.toJavaObject(result, returnType, factory, false, true); } private Object[] unfoldWhenVarargs(java.lang.reflect.Method method, Object[] argParams) { if (null == argParams) { return null; } if (argParams.length == 0 || !method.isVarArgs() || !(argParams[argParams.length - 1] instanceof Object[])) { return argParams; } // when last parameter is Object[] -> unfold the ellipsis: Object[] varargs = (Object[]) argParams[argParams.length - 1]; Object[] args = new Object[argParams.length - 1 + varargs.length]; System.arraycopy(argParams, 0, args, 0, argParams.length - 1); System.arraycopy(varargs, 0, args, argParams.length - 1, varargs.length); return args; } @Override public T queryInterface(Class comInterface) throws COMException { assert COMUtils.comIsInitialized() : "COM not initialized"; try { ComInterface comInterfaceAnnotation = comInterface.getAnnotation(ComInterface.class); if (null == comInterfaceAnnotation) { throw new COMException( "queryInterface: Interface must define a value for iid via the ComInterface annotation"); } final IID iid = this.getIID(comInterfaceAnnotation); final PointerByReference ppvObject = new PointerByReference(); HRESULT hr = ProxyObject.this.getRawDispatch().QueryInterface(new REFIID(iid), ppvObject); if (WinNT.S_OK.equals(hr)) { Dispatch dispatch = new Dispatch(ppvObject.getValue()); T t = this.factory.createProxy(comInterface, dispatch); // QueryInterface returns a COM object pointer with a +1 // reference, we must drop one, // Note: createProxy adds one; int n = dispatch.Release(); return t; } else { String formatMessageFromHR = Kernel32Util.formatMessage(hr); throw new COMException("queryInterface: " + formatMessageFromHR, hr); } } catch (RuntimeException e) { // Do not rewrap COMException if (e instanceof COMException) { throw e; } else { throw new COMException("Error occured when trying to query for interface " + comInterface.getName(), e); } } } private IID getIID(ComInterface annotation) { String iidStr = annotation.iid(); if (null != iidStr && !iidStr.isEmpty()) { return new IID(iidStr); } else { throw new COMException("ComInterface must define a value for iid"); } } // --------------------- ProxyObject --------------------- private String getAccessorName(java.lang.reflect.Method method, ComProperty prop) { if (prop.name().isEmpty()) { String methName = method.getName(); if (methName.startsWith("get")) { return methName.replaceFirst("get", ""); } else { throw new RuntimeException( "Property Accessor name must start with 'get', or set the anotation 'name' value"); } } else { return prop.name(); } } private String getMutatorName(java.lang.reflect.Method method, ComProperty prop) { if (prop.name().isEmpty()) { String methName = method.getName(); if (methName.startsWith("set")) { return methName.replaceFirst("set", ""); } else { throw new RuntimeException( "Property Mutator name must start with 'set', or set the anotation 'name' value"); } } else { return prop.name(); } } private String getMethodName(java.lang.reflect.Method method, ComMethod meth) { if (meth.name().isEmpty()) { String methName = method.getName(); return methName; } else { return meth.name(); } } @SuppressWarnings("deprecation") protected DISPID resolveDispId(String name) { return resolveDispId(getRawDispatch(), name); } protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult, String name, VARIANT pArg) throws COMException { return this.oleMethod(nType, pvResult, name, new VARIANT[] { pArg }); } protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult, DISPID dispId, VARIANT pArg) throws COMException { return this.oleMethod(nType, pvResult, dispId, new VARIANT[] { pArg }); } protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult, String name) throws COMException { return this.oleMethod(nType, pvResult, name, (VARIANT[]) null); } protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult, DISPID dispId) throws COMException { return this.oleMethod(nType, pvResult, dispId, (VARIANT[]) null); } protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult, String name, VARIANT[] pArgs) throws COMException { return this.oleMethod(nType, pvResult, resolveDispId(name), pArgs); } @SuppressWarnings("deprecation") protected HRESULT oleMethod(final int nType, final VARIANT.ByReference pvResult, final DISPID dispId, VARIANT[] pArgs) throws COMException { return oleMethod(nType, pvResult, getRawDispatch(), dispId, pArgs); } @Deprecated protected DISPID resolveDispId(final IDispatch pDisp, String name) { assert COMUtils.comIsInitialized() : "COM not initialized"; if (pDisp == null) { throw new COMException("pDisp (IDispatch) parameter is null!"); } final WString[] ptName = new WString[]{new WString(name)}; final DISPIDByReference pdispID = new DISPIDByReference(); HRESULT hr = pDisp.GetIDsOfNames( new REFIID(Guid.IID_NULL), ptName, 1, factory.getLCID(), pdispID); COMUtils.checkRC(hr); return pdispID.getValue(); } @Deprecated protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult, IDispatch pDisp, String name, VARIANT pArg) throws COMException { return this.oleMethod(nType, pvResult, pDisp, name, new VARIANT[]{pArg}); } @Deprecated protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult, IDispatch pDisp, DISPID dispId, VARIANT pArg) throws COMException { return this.oleMethod(nType, pvResult, pDisp, dispId, new VARIANT[]{pArg}); } @Deprecated protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult, IDispatch pDisp, String name) throws COMException { return this.oleMethod(nType, pvResult, pDisp, name, (VARIANT[]) null); } @Deprecated protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult, IDispatch pDisp, DISPID dispId) throws COMException { return this.oleMethod(nType, pvResult, pDisp, dispId, (VARIANT[]) null); } @Deprecated protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult, final IDispatch pDisp, String name, VARIANT[] pArgs) throws COMException { return this.oleMethod(nType, pvResult, pDisp, resolveDispId(pDisp, name), pArgs); } @Deprecated protected HRESULT oleMethod(final int nType, final VARIANT.ByReference pvResult, final IDispatch pDisp, final DISPID dispId, VARIANT[] pArgs) throws COMException { assert COMUtils.comIsInitialized() : "COM not initialized"; if (pDisp == null) { throw new COMException("pDisp (IDispatch) parameter is null!"); } // variable declaration int _argsLen = 0; VARIANT[] _args = null; final DISPPARAMS.ByReference dp = new DISPPARAMS.ByReference(); final EXCEPINFO.ByReference pExcepInfo = new EXCEPINFO.ByReference(); final IntByReference puArgErr = new IntByReference(); // make parameter reverse ordering as expected by COM runtime if ((pArgs != null) && (pArgs.length > 0)) { _argsLen = pArgs.length; _args = new VARIANT[_argsLen]; int revCount = _argsLen; for (int i = 0; i < _argsLen; i++) { _args[i] = pArgs[--revCount]; } } // Handle special-case for property-puts! if (nType == OleAuto.DISPATCH_PROPERTYPUT) { dp.setRgdispidNamedArgs(new DISPID[]{OaIdl.DISPID_PROPERTYPUT}); } // Apply "fix" according to // https://www.delphitools.info/2013/04/30/gaining-visual-basic-ole-super-powers/ // https://msdn.microsoft.com/en-us/library/windows/desktop/ms221486(v=vs.85).aspx // // Summary: there are methods in the word typelibrary that require both // PROPERTYGET _and_ METHOD to be set. With only one of these set the call // fails. // // The article from delphitools argues, that automation compatible libraries // need to be compatible with VisualBasic which does not distingish methods // and property getters and will set both flags always. // // The MSDN article advises this behaviour: "[...] Some languages cannot // distinguish between retrieving a property and calling a method. In this //case, you should set the flags DISPATCH_PROPERTYGET and DISPATCH_METHOD. // [...]")) // // This was found when trying to bind InchesToPoints from the _Application // dispatch interface of the MS Word 15 type library // // The signature according the ITypeLib Viewer (OLE/COM Object Viewer): // [id(0x00000172), helpcontext(0x09700172)] // single InchesToPoints([in] single Inches); final int finalNType; if (nType == OleAuto.DISPATCH_METHOD || nType == OleAuto.DISPATCH_PROPERTYGET) { finalNType = OleAuto.DISPATCH_METHOD | OleAuto.DISPATCH_PROPERTYGET; } else { finalNType = nType; } // Build DISPPARAMS if (_argsLen > 0) { dp.setArgs(_args); // write 'DISPPARAMS' structure to memory dp.write(); } HRESULT hr = pDisp.Invoke( dispId, new REFIID(Guid.IID_NULL), factory.getLCID(), new WinDef.WORD(finalNType), dp, pvResult, pExcepInfo, puArgErr); COMUtils.checkRC(hr, pExcepInfo, puArgErr); return hr; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy