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

com.google.protobuf.FieldSet Maven / Gradle / Ivy

// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc.  All rights reserved.
// http://code.google.com/p/protobuf/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package com.google.protobuf;

import java.io.IOException;
import java.util.*;

/**
 * A class which represents an arbitrary set of fields of some message type.
 * This is used to implement {@link DynamicMessage}, and also to represent
 * extensions in {@link GeneratedMessage}.  This class is package-private,
 * since outside users should probably be using {@link DynamicMessage}.
 *
 * @author [email protected] Kenton Varda
 */
final class FieldSet> {
    @SuppressWarnings("unchecked")
    private static final FieldSet DEFAULT_INSTANCE = new FieldSet(true);
    private Map fields;

    /**
     * Construct a new FieldSet.
     */
    private FieldSet() {
        // Use a TreeMap because fields need to be in canonical order when
        // serializing.
        // TODO(kenton):  Maybe use some sort of sparse array instead?  It would
        //   even make sense to store the first 16 or so tags in a flat array
        //   to make DynamicMessage faster.
        fields = new TreeMap();
    }

    /**
     * Construct an empty FieldSet.  This is only used to initialize
     * DEFAULT_INSTANCE.
     */
    private FieldSet(final boolean dummy) {
        this.fields = Collections.emptyMap();
    }

    /**
     * Construct a new FieldSet.
     */
    public static >
    FieldSet newFieldSet() {
        return new FieldSet();
    }

    /**
     * Get an immutable empty FieldSet.
     */
    @SuppressWarnings("unchecked")
    public static >
    FieldSet emptySet() {
        return DEFAULT_INSTANCE;
    }

    /**
     * Verifies that the given object is of the correct type to be a valid
     * value for the given field.  (For repeated fields, this checks if the
     * object is the right type to be one element of the field.)
     *
     * @throws IllegalArgumentException The value is not of the right type.
     */
    private static void verifyType(final WireFormat.FieldType type,
                                   final Object value) {
        if (value == null) {
            throw new NullPointerException();
        }

        boolean isValid = false;
        switch (type.getJavaType()) {
            case INT:
                isValid = value instanceof Integer;
                break;
            case LONG:
                isValid = value instanceof Long;
                break;
            case FLOAT:
                isValid = value instanceof Float;
                break;
            case DOUBLE:
                isValid = value instanceof Double;
                break;
            case BOOLEAN:
                isValid = value instanceof Boolean;
                break;
            case STRING:
                isValid = value instanceof String;
                break;
            case BYTE_STRING:
                isValid = value instanceof ByteString;
                break;
            case ENUM:
                // TODO(kenton):  Caller must do type checking here, I guess.
                isValid = value instanceof Internal.EnumLite;
                break;
            case MESSAGE:
                // TODO(kenton):  Caller must do type checking here, I guess.
                isValid = value instanceof MessageLite;
                break;
        }

        if (!isValid) {
            // TODO(kenton):  When chaining calls to setField(), it can be hard to
            //   tell from the stack trace which exact call failed, since the whole
            //   chain is considered one line of code.  It would be nice to print
            //   more information here, e.g. naming the field.  We used to do that.
            //   But we can't now that FieldSet doesn't use descriptors.  Maybe this
            //   isn't a big deal, though, since it would only really apply when using
            //   reflection and generally people don't chain reflection setters.
            throw new IllegalArgumentException(
                    "Wrong object type used with protocol message reflection.");
        }
    }

    /**
     * Given a field type, return the wire type.
     *
     * @returns One of the {@code WIRETYPE_} constants defined in
     * {@link WireFormat}.
     */
    static int getWireFormatForFieldType(final WireFormat.FieldType type,
                                         boolean isPacked) {
        if (isPacked) {
            return WireFormat.WIRETYPE_LENGTH_DELIMITED;
        } else {
            return type.getWireType();
        }
    }

    // =================================================================

    /**
     * Read a field of any primitive type from a CodedInputStream.  Enums,
     * groups, and embedded messages are not handled by this method.
     *
     * @param input The stream from which to read.
     * @param type  Declared type of the field.
     * @return An object representing the field's value, of the exact
     * type which would be returned by
     * {@link Message#getField(Descriptors.FieldDescriptor)} for
     * this field.
     */
    public static Object readPrimitiveField(
            CodedInputStream input,
            final WireFormat.FieldType type) throws IOException {
        switch (type) {
            case DOUBLE:
                return input.readDouble();
            case FLOAT:
                return input.readFloat();
            case INT64:
                return input.readInt64();
            case UINT64:
                return input.readUInt64();
            case INT32:
                return input.readInt32();
            case FIXED64:
                return input.readFixed64();
            case FIXED32:
                return input.readFixed32();
            case BOOL:
                return input.readBool();
            case STRING:
                return input.readString();
            case BYTES:
                return input.readBytes();
            case UINT32:
                return input.readUInt32();
            case SFIXED32:
                return input.readSFixed32();
            case SFIXED64:
                return input.readSFixed64();
            case SINT32:
                return input.readSInt32();
            case SINT64:
                return input.readSInt64();

            case GROUP:
                throw new IllegalArgumentException(
                        "readPrimitiveField() cannot handle nested groups.");
            case MESSAGE:
                throw new IllegalArgumentException(
                        "readPrimitiveField() cannot handle embedded messages.");
            case ENUM:
                // We don't handle enums because we don't know what to do if the
                // value is not recognized.
                throw new IllegalArgumentException(
                        "readPrimitiveField() cannot handle enums.");
        }

        throw new RuntimeException(
                "There is no way to get here, but the compiler thinks otherwise.");
    }

    /**
     * Write a single tag-value pair to the stream.
     *
     * @param output The output stream.
     * @param type   The field's type.
     * @param number The field's number.
     * @param value  Object representing the field's value.  Must be of the exact
     *               type which would be returned by
     *               {@link Message#getField(Descriptors.FieldDescriptor)} for
     *               this field.
     */
    private static void writeElement(final CodedOutputStream output,
                                     final WireFormat.FieldType type,
                                     final int number,
                                     final Object value) throws IOException {
        // Special case for groups, which need a start and end tag; other fields
        // can just use writeTag() and writeFieldNoTag().
        if (type == WireFormat.FieldType.GROUP) {
            output.writeGroup(number, (MessageLite) value);
        } else {
            output.writeTag(number, getWireFormatForFieldType(type, false));
            writeElementNoTag(output, type, value);
        }
    }

    /**
     * Write a field of arbitrary type, without its tag, to the stream.
     *
     * @param output The output stream.
     * @param type   The field's type.
     * @param value  Object representing the field's value.  Must be of the exact
     *               type which would be returned by
     *               {@link Message#getField(Descriptors.FieldDescriptor)} for
     *               this field.
     */
    private static void writeElementNoTag(
            final CodedOutputStream output,
            final WireFormat.FieldType type,
            final Object value) throws IOException {
        switch (type) {
            case DOUBLE:
                output.writeDoubleNoTag((Double) value);
                break;
            case FLOAT:
                output.writeFloatNoTag((Float) value);
                break;
            case INT64:
                output.writeInt64NoTag((Long) value);
                break;
            case UINT64:
                output.writeUInt64NoTag((Long) value);
                break;
            case INT32:
                output.writeInt32NoTag((Integer) value);
                break;
            case FIXED64:
                output.writeFixed64NoTag((Long) value);
                break;
            case FIXED32:
                output.writeFixed32NoTag((Integer) value);
                break;
            case BOOL:
                output.writeBoolNoTag((Boolean) value);
                break;
            case STRING:
                output.writeStringNoTag((String) value);
                break;
            case GROUP:
                output.writeGroupNoTag((MessageLite) value);
                break;
            case MESSAGE:
                output.writeMessageNoTag((MessageLite) value);
                break;
            case BYTES:
                output.writeBytesNoTag((ByteString) value);
                break;
            case UINT32:
                output.writeUInt32NoTag((Integer) value);
                break;
            case SFIXED32:
                output.writeSFixed32NoTag((Integer) value);
                break;
            case SFIXED64:
                output.writeSFixed64NoTag((Long) value);
                break;
            case SINT32:
                output.writeSInt32NoTag((Integer) value);
                break;
            case SINT64:
                output.writeSInt64NoTag((Long) value);
                break;

            case ENUM:
                output.writeEnumNoTag(((Internal.EnumLite) value).getNumber());
                break;
        }
    }

    /**
     * Write a single field.
     */
    public static void writeField(final FieldDescriptorLite descriptor,
                                  final Object value,
                                  final CodedOutputStream output)
            throws IOException {
        WireFormat.FieldType type = descriptor.getLiteType();
        int number = descriptor.getNumber();
        if (descriptor.isRepeated()) {
            final List valueList = (List) value;
            if (descriptor.isPacked()) {
                output.writeTag(number, WireFormat.WIRETYPE_LENGTH_DELIMITED);
                // Compute the total data size so the length can be written.
                int dataSize = 0;
                for (final Object element : valueList) {
                    dataSize += computeElementSizeNoTag(type, element);
                }
                output.writeRawVarint32(dataSize);
                // Write the data itself, without any tags.
                for (final Object element : valueList) {
                    writeElementNoTag(output, type, element);
                }
            } else {
                for (final Object element : valueList) {
                    writeElement(output, type, number, element);
                }
            }
        } else {
            writeElement(output, type, number, value);
        }
    }

    /**
     * Compute the number of bytes that would be needed to encode a
     * single tag/value pair of arbitrary type.
     *
     * @param type   The field's type.
     * @param number The field's number.
     * @param value  Object representing the field's value.  Must be of the exact
     *               type which would be returned by
     *               {@link Message#getField(Descriptors.FieldDescriptor)} for
     *               this field.
     */
    private static int computeElementSize(
            final WireFormat.FieldType type,
            final int number, final Object value) {
        int tagSize = CodedOutputStream.computeTagSize(number);
        if (type == WireFormat.FieldType.GROUP) {
            tagSize *= 2;
        }
        return tagSize + computeElementSizeNoTag(type, value);
    }

    /**
     * Compute the number of bytes that would be needed to encode a
     * particular value of arbitrary type, excluding tag.
     *
     * @param type  The field's type.
     * @param value Object representing the field's value.  Must be of the exact
     *              type which would be returned by
     *              {@link Message#getField(Descriptors.FieldDescriptor)} for
     *              this field.
     */
    private static int computeElementSizeNoTag(
            final WireFormat.FieldType type, final Object value) {
        switch (type) {
            // Note:  Minor violation of 80-char limit rule here because this would
            //   actually be harder to read if we wrapped the lines.
            case DOUBLE:
                return CodedOutputStream.computeDoubleSizeNoTag((Double) value);
            case FLOAT:
                return CodedOutputStream.computeFloatSizeNoTag((Float) value);
            case INT64:
                return CodedOutputStream.computeInt64SizeNoTag((Long) value);
            case UINT64:
                return CodedOutputStream.computeUInt64SizeNoTag((Long) value);
            case INT32:
                return CodedOutputStream.computeInt32SizeNoTag((Integer) value);
            case FIXED64:
                return CodedOutputStream.computeFixed64SizeNoTag((Long) value);
            case FIXED32:
                return CodedOutputStream.computeFixed32SizeNoTag((Integer) value);
            case BOOL:
                return CodedOutputStream.computeBoolSizeNoTag((Boolean) value);
            case STRING:
                return CodedOutputStream.computeStringSizeNoTag((String) value);
            case GROUP:
                return CodedOutputStream.computeGroupSizeNoTag((MessageLite) value);
            case MESSAGE:
                return CodedOutputStream.computeMessageSizeNoTag((MessageLite) value);
            case BYTES:
                return CodedOutputStream.computeBytesSizeNoTag((ByteString) value);
            case UINT32:
                return CodedOutputStream.computeUInt32SizeNoTag((Integer) value);
            case SFIXED32:
                return CodedOutputStream.computeSFixed32SizeNoTag((Integer) value);
            case SFIXED64:
                return CodedOutputStream.computeSFixed64SizeNoTag((Long) value);
            case SINT32:
                return CodedOutputStream.computeSInt32SizeNoTag((Integer) value);
            case SINT64:
                return CodedOutputStream.computeSInt64SizeNoTag((Long) value);

            case ENUM:
                return CodedOutputStream.computeEnumSizeNoTag(
                        ((Internal.EnumLite) value).getNumber());
        }

        throw new RuntimeException(
                "There is no way to get here, but the compiler thinks otherwise.");
    }

    /**
     * Compute the number of bytes needed to encode a particular field.
     */
    public static int computeFieldSize(final FieldDescriptorLite descriptor,
                                       final Object value) {
        WireFormat.FieldType type = descriptor.getLiteType();
        int number = descriptor.getNumber();
        if (descriptor.isRepeated()) {
            if (descriptor.isPacked()) {
                int dataSize = 0;
                for (final Object element : (List) value) {
                    dataSize += computeElementSizeNoTag(type, element);
                }
                return dataSize +
                        CodedOutputStream.computeTagSize(number) +
                        CodedOutputStream.computeRawVarint32Size(dataSize);
            } else {
                int size = 0;
                for (final Object element : (List) value) {
                    size += computeElementSize(type, number, element);
                }
                return size;
            }
        } else {
            return computeElementSize(type, number, value);
        }
    }

    /**
     * Make this FieldSet immutable from this point forward.
     */
    @SuppressWarnings("unchecked")
    public void makeImmutable() {
        for (final Map.Entry entry :
                fields.entrySet()) {
            if (entry.getKey().isRepeated()) {
                final List value = (List) entry.getValue();
                fields.put(entry.getKey(), Collections.unmodifiableList(value));
            }
        }
        fields = Collections.unmodifiableMap(fields);
    }

    /**
     * See {@link Message.Builder#clear()}.
     */
    public void clear() {
        fields.clear();
    }

    /**
     * Get a simple map containing all the fields.
     */
    public Map getAllFields() {
        return Collections.unmodifiableMap(fields);
    }

    /**
     * Get an iterator to the field map.  This iterator should not be leaked
     * out of the protobuf library as it is not protected from mutation.
     */
    public Iterator> iterator() {
        return fields.entrySet().iterator();
    }

    /**
     * Useful for implementing
     * {@link Message#hasField(Descriptors.FieldDescriptor)}.
     */
    public boolean hasField(final FieldDescriptorType descriptor) {
        if (descriptor.isRepeated()) {
            throw new IllegalArgumentException(
                    "hasField() can only be called on non-repeated fields.");
        }

        return fields.get(descriptor) != null;
    }

    // =================================================================
    // Parsing and serialization

    /**
     * Useful for implementing
     * {@link Message#getField(Descriptors.FieldDescriptor)}.  This method
     * returns {@code null} if the field is not set; in this case it is up
     * to the caller to fetch the field's default value.
     */
    public Object getField(final FieldDescriptorType descriptor) {
        return fields.get(descriptor);
    }

    /**
     * Useful for implementing
     * {@link Message.Builder#setField(Descriptors.FieldDescriptor, Object)}.
     */
    @SuppressWarnings("unchecked")
    public void setField(final FieldDescriptorType descriptor,
                         Object value) {
        if (descriptor.isRepeated()) {
            if (!(value instanceof List)) {
                throw new IllegalArgumentException(
                        "Wrong object type used with protocol message reflection.");
            }

            // Wrap the contents in a new list so that the caller cannot change
            // the list's contents after setting it.
            final List newList = new ArrayList();
            newList.addAll((List) value);
            for (final Object element : newList) {
                verifyType(descriptor.getLiteType(), element);
            }
            value = newList;
        } else {
            verifyType(descriptor.getLiteType(), value);
        }

        fields.put(descriptor, value);
    }

    /**
     * Useful for implementing
     * {@link Message.Builder#clearField(Descriptors.FieldDescriptor)}.
     */
    public void clearField(final FieldDescriptorType descriptor) {
        fields.remove(descriptor);
    }

    // TODO(kenton):  Move static parsing and serialization methods into some
    //   other class.  Probably WireFormat.

    /**
     * Useful for implementing
     * {@link Message#getRepeatedFieldCount(Descriptors.FieldDescriptor)}.
     */
    public int getRepeatedFieldCount(final FieldDescriptorType descriptor) {
        if (!descriptor.isRepeated()) {
            throw new IllegalArgumentException(
                    "getRepeatedField() can only be called on repeated fields.");
        }

        final Object value = fields.get(descriptor);
        if (value == null) {
            return 0;
        } else {
            return ((List) value).size();
        }
    }

    /**
     * Useful for implementing
     * {@link Message#getRepeatedField(Descriptors.FieldDescriptor, int)}.
     */
    public Object getRepeatedField(final FieldDescriptorType descriptor,
                                   final int index) {
        if (!descriptor.isRepeated()) {
            throw new IllegalArgumentException(
                    "getRepeatedField() can only be called on repeated fields.");
        }

        final Object value = fields.get(descriptor);

        if (value == null) {
            throw new IndexOutOfBoundsException();
        } else {
            return ((List) value).get(index);
        }
    }

    /**
     * Useful for implementing
     * {@link Message.Builder#setRepeatedField(Descriptors.FieldDescriptor, int, Object)}.
     */
    @SuppressWarnings("unchecked")
    public void setRepeatedField(final FieldDescriptorType descriptor,
                                 final int index,
                                 final Object value) {
        if (!descriptor.isRepeated()) {
            throw new IllegalArgumentException(
                    "getRepeatedField() can only be called on repeated fields.");
        }

        final Object list = fields.get(descriptor);
        if (list == null) {
            throw new IndexOutOfBoundsException();
        }

        verifyType(descriptor.getLiteType(), value);
        ((List) list).set(index, value);
    }

    /**
     * Useful for implementing
     * {@link Message.Builder#addRepeatedField(Descriptors.FieldDescriptor, Object)}.
     */
    @SuppressWarnings("unchecked")
    public void addRepeatedField(final FieldDescriptorType descriptor,
                                 final Object value) {
        if (!descriptor.isRepeated()) {
            throw new IllegalArgumentException(
                    "addRepeatedField() can only be called on repeated fields.");
        }

        verifyType(descriptor.getLiteType(), value);

        final Object existingValue = fields.get(descriptor);
        List list;
        if (existingValue == null) {
            list = new ArrayList();
            fields.put(descriptor, list);
        } else {
            list = (List) existingValue;
        }

        list.add(value);
    }

    /**
     * See {@link Message#isInitialized()}.  Note:  Since {@code FieldSet}
     * itself does not have any way of knowing about required fields that
     * aren't actually present in the set, it is up to the caller to check
     * that all required fields are present.
     */
    @SuppressWarnings("unchecked")
    public boolean isInitialized() {
        for (final Map.Entry entry :
                fields.entrySet()) {
            final FieldDescriptorType descriptor = entry.getKey();
            if (descriptor.getLiteJavaType() == WireFormat.JavaType.MESSAGE) {
                if (descriptor.isRepeated()) {
                    for (final MessageLite element :
                            (List) entry.getValue()) {
                        if (!element.isInitialized()) {
                            return false;
                        }
                    }
                } else {
                    if (!((MessageLite) entry.getValue()).isInitialized()) {
                        return false;
                    }
                }
            }
        }

        return true;
    }

    /**
     * Like {@link #mergeFrom(Message)}, but merges from another {@link FieldSet}.
     */
    @SuppressWarnings("unchecked")
    public void mergeFrom(final FieldSet other) {
        for (final Map.Entry entry :
                other.fields.entrySet()) {
            final FieldDescriptorType descriptor = entry.getKey();
            final Object otherValue = entry.getValue();

            if (descriptor.isRepeated()) {
                Object value = fields.get(descriptor);
                if (value == null) {
                    // Our list is empty, but we still need to make a defensive copy of
                    // the other list since we don't know if the other FieldSet is still
                    // mutable.
                    fields.put(descriptor, new ArrayList((List) otherValue));
                } else {
                    // Concatenate the lists.
                    ((List) value).addAll((List) otherValue);
                }
            } else if (descriptor.getLiteJavaType() == WireFormat.JavaType.MESSAGE) {
                Object value = fields.get(descriptor);
                if (value == null) {
                    fields.put(descriptor, otherValue);
                } else {
                    // Merge the messages.
                    fields.put(descriptor,
                            descriptor.internalMergeFrom(
                                    ((MessageLite) value).toBuilder(), (MessageLite) otherValue)
                                    .build());
                }

            } else {
                fields.put(descriptor, otherValue);
            }
        }
    }

    /**
     * See {@link Message#writeTo(CodedOutputStream)}.
     */
    public void writeTo(final CodedOutputStream output)
            throws IOException {
        for (final Map.Entry entry :
                fields.entrySet()) {
            writeField(entry.getKey(), entry.getValue(), output);
        }
    }

    /**
     * Like {@link #writeTo} but uses MessageSet wire format.
     */
    public void writeMessageSetTo(final CodedOutputStream output)
            throws IOException {
        for (final Map.Entry entry :
                fields.entrySet()) {
            final FieldDescriptorType descriptor = entry.getKey();
            if (descriptor.getLiteJavaType() == WireFormat.JavaType.MESSAGE &&
                    !descriptor.isRepeated() && !descriptor.isPacked()) {
                output.writeMessageSetExtension(entry.getKey().getNumber(),
                        (MessageLite) entry.getValue());
            } else {
                writeField(descriptor, entry.getValue(), output);
            }
        }
    }

    /**
     * See {@link Message#getSerializedSize()}.  It's up to the caller to cache
     * the resulting size if desired.
     */
    public int getSerializedSize() {
        int size = 0;
        for (final Map.Entry entry :
                fields.entrySet()) {
            size += computeFieldSize(entry.getKey(), entry.getValue());
        }
        return size;
    }

    /**
     * Like {@link #getSerializedSize} but uses MessageSet wire format.
     */
    public int getMessageSetSerializedSize() {
        int size = 0;
        for (final Map.Entry entry :
                fields.entrySet()) {
            final FieldDescriptorType descriptor = entry.getKey();
            if (descriptor.getLiteJavaType() == WireFormat.JavaType.MESSAGE &&
                    !descriptor.isRepeated() && !descriptor.isPacked()) {
                size += CodedOutputStream.computeMessageSetExtensionSize(
                        entry.getKey().getNumber(), (MessageLite) entry.getValue());
            } else {
                size += computeFieldSize(descriptor, entry.getValue());
            }
        }
        return size;
    }

    /**
     * Interface for a FieldDescriptor or lite extension descriptor.  This
     * prevents FieldSet from depending on {@link Descriptors.FieldDescriptor}.
     */
    public interface FieldDescriptorLite>
            extends Comparable {
        int getNumber();

        WireFormat.FieldType getLiteType();

        WireFormat.JavaType getLiteJavaType();

        boolean isRepeated();

        boolean isPacked();

        Internal.EnumLiteMap getEnumType();

        // If getLiteJavaType() == MESSAGE, this merges a message object of the
        // type into a builder of the type.  Returns {@code to}.
        MessageLite.Builder internalMergeFrom(
                MessageLite.Builder to, MessageLite from);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy