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

com.launchdarkly.sdk.internal.events.EventContextFormatter Maven / Gradle / Ivy

package com.launchdarkly.sdk.internal.events;

import com.google.gson.stream.JsonWriter;
import com.launchdarkly.sdk.AttributeRef;
import com.launchdarkly.sdk.LDContext;
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.LDValueType;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import static com.launchdarkly.sdk.internal.GsonHelpers.gsonInstance;

/**
 * Implements serialization of contexts within JSON event data. This uses a similar schema to the
 * regular context JSON schema (i.e. what you get if you call JsonSerialization.serialize() on an
 * LDContext), but not quite the same, because it transforms the context to redact any attributes
 * (or subproperties of attributes that are objects) that were designated as private, accumulating
 * a list of the names of these in _meta.redactedAttributes.
 * 

* This implementation is optimized to avoid unnecessary work in the typical use case where there * aren't any private attributes. */ class EventContextFormatter { private final boolean allAttributesPrivate; private final AttributeRef[] globalPrivateAttributes; EventContextFormatter(boolean allAttributesPrivate, AttributeRef[] globalPrivateAttributes) { this.allAttributesPrivate = allAttributesPrivate; this.globalPrivateAttributes = globalPrivateAttributes == null ? new AttributeRef[0] : globalPrivateAttributes; } public void write(LDContext c, JsonWriter w) throws IOException { if (c.isMultiple()) { w.beginObject(); w.name("kind").value("multi"); for (int i = 0; i < c.getIndividualContextCount(); i++) { LDContext c1 = c.getIndividualContext(i); w.name(c1.getKind().toString()); writeSingleKind(c1, w, false); } w.endObject(); } else { writeSingleKind(c, w, true); } } private void writeSingleKind(LDContext c, JsonWriter w, boolean includeKind) throws IOException { w.beginObject(); // kind, key, and anonymous are never redacted if (includeKind) { w.name("kind").value(c.getKind().toString()); } w.name("key").value(c.getKey()); if (c.isAnonymous()) { w.name("anonymous").value(true); } List redacted = null; if (c.getName() != null) { if (isAttributeEntirelyPrivate(c, "name")) { redacted = addOrCreate(redacted, "name"); } else { w.name("name").value(c.getName()); } } for (String attrName: c.getCustomAttributeNames()) { redacted = writeOrRedactAttribute(w, c, attrName, c.getValue(attrName), redacted); } boolean haveRedacted = redacted != null && !redacted.isEmpty(); if (haveRedacted) { w.name("_meta").beginObject(); w.name("redactedAttributes").beginArray(); for (String a: redacted) { w.value(a); } w.endArray(); w.endObject(); } w.endObject(); } private boolean isAttributeEntirelyPrivate(LDContext c, String attrName) { if (allAttributesPrivate) { return true; } AttributeRef privateRef = findPrivateRef(c, 1, attrName, null); return privateRef != null && privateRef.getDepth() == 1; } private List writeOrRedactAttribute( JsonWriter w, LDContext c, String attrName, LDValue value, List redacted ) throws IOException { if (allAttributesPrivate) { return addOrCreate(redacted, attrName); } return writeRedactedValue(w, c, 0, attrName, value, null, redacted); } // This method implements the context-aware attribute redaction logic, in which an attribute // can be 1. written as-is, 2. fully redacted, or 3. (for a JSON object) partially redacted. // It returns the updated redacted attribute list. private List writeRedactedValue( JsonWriter w, LDContext c, int previousDepth, String attrName, LDValue value, AttributeRef previousMatchRef, List redacted ) throws IOException { // See findPrivateRef for the meaning of the previousMatchRef parameter. int depth = previousDepth + 1; AttributeRef privateRef = findPrivateRef(c, depth, attrName, previousMatchRef); // If privateRef is non-null, then it is either an exact match for the property we're looking at, // or it refers to a subproperty of it (for instance, if we are redacting property "b" within // attribute "a", it could be /a/b [depth 2] or /a/b/c [depth 3]). If the depth shows that it's an // exact match, this whole value is redacted and we don't bother recursing. if (privateRef != null && privateRef.getDepth() == depth) { return addOrCreate(redacted, privateRef.toString()); } // If privateRef is null (there was no matching private attribute)-- or, if privateRef isn't null // but it refers to a subproperty, and this value isn't an object so it has no properties-- then // we just write the value unredacted. if (privateRef == null || value.getType() != LDValueType.OBJECT) { writeNameAndValue(w, attrName, value); return redacted; } // At this point we know it is an object and we are redacting subproperties. w.name(attrName).beginObject(); for (String name: value.keys()) { redacted = writeRedactedValue(w, c, depth, name, value.get(name), privateRef, redacted); } w.endObject(); return redacted; } // Searches both the globally private attributes and the per-context private attributes to find a // match for the attribute or subproperty we're looking at. // // If we find one that exactly matches the current path (that is, the depth is the same), we // return that one, because that would tell us that the entire attribute/subproperty should be // redacted. If we don't find that, but we do find at least one match for a subproperty of this // path (that is, it has the current path as a prefix, but the depth is greater), then we return // it, to tell us that we'll need to recurse to redact subproperties. // // The previousMatchRef parameter is how we to keep track of the previous path segments we have // already matched when recursing. It starts out as null at the top level. Then, every time we // recurse to redact subproperties of an object, we set previousMatchRef to *any* AttributeRef // we've seen that has the current subpath as a prefix; such an AttributeRef is guaranteed to // exist, because we wouldn't have bothered to recurse if we hadn't found one, and we will only // be comparing components 0 through depth-1 of it (see matchPrivateRef). This shortcut allows // us to avoid allocating a variable-length mutable data structure such as a stack. private AttributeRef findPrivateRef(LDContext c, int depth, String attrName, AttributeRef previousMatchRef) { AttributeRef nonExactMatch = null; if (globalPrivateAttributes.length != 0) { // minor optimization to avoid creating an iterator if it's empty for (AttributeRef globalPrivate: globalPrivateAttributes) { if (matchPrivateRef(globalPrivate, depth, attrName, previousMatchRef)) { if (globalPrivate.getDepth() == depth) { return globalPrivate; } nonExactMatch = globalPrivate; } } } for (int i = 0; i < c.getPrivateAttributeCount(); i++) { AttributeRef contextPrivate = c.getPrivateAttribute(i); if (matchPrivateRef(contextPrivate, depth, attrName, previousMatchRef)) { if (contextPrivate.getDepth() == depth) { return contextPrivate; } nonExactMatch = contextPrivate; } } return nonExactMatch; } private static boolean matchPrivateRef(AttributeRef ref, int depth, String attrName, AttributeRef previousMatchRef) { if (ref.getDepth() < depth) { return false; } for (int i = 0; i < (depth - 1); i++) { if (!ref.getComponent(i).equals(previousMatchRef.getComponent(i))) { return false; } } return ref.getComponent(depth - 1).equals(attrName); } private static void writeNameAndValue(JsonWriter w, String name, LDValue value) throws IOException { w.name(name); gsonInstance().toJson(value, LDValue.class, w); } private static List addOrCreate(List list, T value) { if (list == null) { list = new ArrayList<>(); } list.add(value); return list; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy