Home / Type/ JsonWriter Type — spring-boot Architecture

JsonWriter Type — spring-boot Architecture

Architecture documentation for the JsonWriter type/interface in JsonWriter.java from the spring-boot codebase.

Entity Profile

Source Code

core/spring-boot/src/main/java/org/springframework/boot/json/JsonWriter.java lines 79–1082

@FunctionalInterface
public interface JsonWriter<T> {

	/**
	 * Write the given instance to the provided {@link Appendable}.
	 * @param instance the instance to write (may be {@code null}
	 * @param out the output that should receive the JSON
	 * @throws IOException on IO error
	 */
	void write(@Nullable T instance, Appendable out) throws IOException;

	/**
	 * Write the given instance to a JSON string.
	 * @param instance the instance to write (may be {@code null})
	 * @return the JSON string
	 */
	default String writeToString(@Nullable T instance) {
		return write(instance).toJsonString();
	}

	/**
	 * Provide a {@link WritableJson} implementation that may be used to write the given
	 * instance to various outputs.
	 * @param instance the instance to write (may be {@code null})
	 * @return a {@link WritableJson} instance that may be used to write the JSON
	 */
	default WritableJson write(@Nullable T instance) {
		return WritableJson.of((out) -> write(instance, out));
	}

	/**
	 * Return a new {@link JsonWriter} instance that appends a new line after the JSON has
	 * been written.
	 * @return a new {@link JsonWriter} instance that appends a new line after the JSON
	 */
	default JsonWriter<T> withNewLineAtEnd() {
		return withSuffix("\n");
	}

	/**
	 * Return a new {@link JsonWriter} instance that appends the given suffix after the
	 * JSON has been written.
	 * @param suffix the suffix to write, if any
	 * @return a new {@link JsonWriter} instance that appends a suffix after the JSON
	 */
	default JsonWriter<T> withSuffix(@Nullable String suffix) {
		if (!StringUtils.hasLength(suffix)) {
			return this;
		}
		return (instance, out) -> {
			write(instance, out);
			out.append(suffix);
		};
	}

	/**
	 * Factory method to return a {@link JsonWriter} for standard Java types. See
	 * {@link JsonValueWriter class-level javadoc} for details.
	 * @param <T> the type to write
	 * @return a {@link JsonWriter} instance
	 */
	static <T> JsonWriter<T> standard() {
		return of(Members::add);
	}

	/**
	 * Factory method to return a {@link JsonWriter} with specific {@link Members member
	 * mapping}. See {@link JsonValueWriter class-level javadoc} and {@link Members} for
	 * details.
	 * @param <T> the type to write
	 * @param members a consumer, which should configure the members
	 * @return a {@link JsonWriter} instance
	 * @see Members
	 */
	static <T> JsonWriter<T> of(Consumer<Members<T>> members) {
		// Don't inline 'new Members' (must be outside of lambda)
		Members<T> initializedMembers = new Members<>(members, false);
		return (instance, out) -> initializedMembers.write(instance, new JsonValueWriter(out));
	}

	/**
	 * Callback used to configure JSON members. Individual members can be declared using
	 * the various {@code add(...)} methods. Typically, members are declared with a
	 * {@code "name"} and a {@link Function} that will extract the value from the
	 * instance. Members can also be declared using a static value or a {@link Supplier}.
	 * The {@link #add(String)} and {@link #add()} methods may be used to access the
	 * actual instance being written.
	 * <p>
	 * Members can be added without a {@code name} when a {@code Member.using(...)} method
	 * is used to complete the definition.
	 * <p>
	 * Members can filtered using {@code Member.when} methods and adapted to different
	 * types using {@link Member#as(Extractor) Member.as(...)}.
	 *
	 * @param <T> the type that will be written
	 */
	final class Members<T> {

		private final List<Member<?>> members = new ArrayList<>();

		private final boolean contributesPair;

		private final @Nullable Series series;

		private final JsonWriterFiltersAndProcessors jsonProcessors = new JsonWriterFiltersAndProcessors();

		Members(Consumer<Members<T>> members, boolean contributesToExistingSeries) {
			Assert.notNull(members, "'members' must not be null");
			members.accept(this);
			Assert.state(!this.members.isEmpty(), "No members have been added");
			this.contributesPair = this.members.stream().anyMatch(Member::contributesPair);
			this.series = (this.contributesPair && !contributesToExistingSeries) ? Series.OBJECT : null;
			if (this.contributesPair || this.members.size() > 1) {
				this.members.forEach((member) -> Assert.state(member.contributesPair(),
						() -> String.format("%s does not contribute a named pair, ensure that all members have "
								+ "a name or call an appropriate 'using' method", member)));
			}
		}

		/**
		 * Add a new member with access to the instance being written.
		 * @param name the member name
		 * @return the added {@link Member} which may be configured further
		 */
		public Member<T> add(String name) {
			return add(name, (instance) -> instance);
		}

		/**
		 * Add a new member with a static value.
		 * @param <V> the value type
		 * @param name the member name
		 * @param value the member value
		 * @return the added {@link Member} which may be configured further
		 */
		public <V> Member<V> add(String name, @Nullable V value) {
			return add(name, (instance) -> value);
		}

		/**
		 * Add a new member with a supplied value.
		 * @param <V> the value type
		 * @param name the member name
		 * @param supplier a supplier of the value
		 * @return the added {@link Member} which may be configured further
		 */
		public <V> Member<V> add(String name, Supplier<@Nullable V> supplier) {
			Assert.notNull(supplier, "'supplier' must not be null");
			return add(name, (instance) -> supplier.get());
		}

		/**
		 * Add a new member with an extracted value.
		 * @param <V> the value type
		 * @param name the member name
		 * @param extractor {@link Extractor} to extract the value
		 * @return the added {@link Member} which may be configured further
		 */
		public <V> Member<V> add(String name, Extractor<T, V> extractor) {
			Assert.notNull(name, "'name' must not be null");
			Assert.notNull(extractor, "'extractor' must not be null");
			return addMember(name, extractor);
		}

		/**
		 * Add a new member with access to the instance being written. The member is added
		 * without a name, so one of the {@code Member.using(...)} methods must be used to
		 * complete the configuration.
		 * @return the added {@link Member} which may be configured further
		 */
		public Member<T> add() {
			return from((value) -> value);
		}

		/**
		 * Add all entries from the given {@link Map} to the JSON.
		 * @param <M> the map type
		 * @param <K> the key type
		 * @param <V> the value type
		 * @param extractor {@link Extractor} to extract the map
		 * @return the added {@link Member} which may be configured further
		 */
		public <M extends Map<K, V>, K, V> Member<M> addMapEntries(Extractor<T, M> extractor) {
			return from(extractor).usingPairs(Map::forEach);
		}

		/**
		 * Add members from a static value. One of the {@code Member.using(...)} methods
		 * must be used to complete the configuration.
		 * @param <V> the value type
		 * @param value the member value
		 * @return the added {@link Member} which may be configured further
		 */
		public <V> Member<V> from(@Nullable V value) {
			return from((instance) -> value);
		}

		/**
		 * Add members from a supplied value. One of the {@code Member.using(...)} methods
		 * must be used to complete the configuration.
		 * @param <V> the value type
		 * @param supplier a supplier of the value
		 * @return the added {@link Member} which may be configured further
		 */
		public <V> Member<V> from(Supplier<@Nullable V> supplier) {
			Assert.notNull(supplier, "'supplier' must not be null");
			return from((instance) -> supplier.get());
		}

		/**
		 * Add members from an extracted value. One of the {@code Member.using(...)}
		 * methods must be used to complete the configuration.
		 * @param <V> the value type
		 * @param extractor {@link Extractor} to extract the value
		 * @return the added {@link Member} which may be configured further
		 */
		public <V> Member<V> from(Extractor<T, V> extractor) {
			Assert.notNull(extractor, "'extractor' must not be null");
			return addMember(null, extractor);
		}

		/**
		 * Add a filter that will be used to restrict the members written to the JSON.
		 * @param predicate the predicate used to filter members
		 */
		public void applyingPathFilter(Predicate<MemberPath> predicate) {
			Assert.notNull(predicate, "'predicate' must not be null");
			this.jsonProcessors.pathFilters().add(predicate);
		}

		/**
		 * Add a {@link NameProcessor} to be applied when the JSON is written.
		 * @param nameProcessor the name processor to add
		 */
		public void applyingNameProcessor(NameProcessor nameProcessor) {
			Assert.notNull(nameProcessor, "'nameProcessor' must not be null");
			this.jsonProcessors.nameProcessors().add(nameProcessor);
		}

		/**
		 * Add a {@link ValueProcessor} to be applied when the JSON is written.
		 * @param valueProcessor the value processor to add
		 */
		public void applyingValueProcessor(ValueProcessor<?> valueProcessor) {
			Assert.notNull(valueProcessor, "'valueProcessor' must not be null");
			this.jsonProcessors.valueProcessors().add(valueProcessor);
		}

		private <V> Member<V> addMember(@Nullable String name, Extractor<T, V> extractor) {
			Member<V> member = new Member<>(this.members.size(), name, ValueExtractor.of(extractor));
			this.members.add(member);
			return member;
		}

		/**
		 * Writes the given instance using the configured {@link Member members}.
		 * @param instance the instance to write
		 * @param valueWriter the JSON value writer to use
		 */
		void write(@Nullable T instance, JsonValueWriter valueWriter) {
			valueWriter.pushProcessors(this.jsonProcessors);
			valueWriter.start(this.series);
			for (Member<?> member : this.members) {
				member.write(instance, valueWriter);
			}
			valueWriter.end(this.series);
			valueWriter.popProcessors();
		}

		/**
		 * Return if any of the members contributes a name/value pair to the JSON.
		 * @return if a name/value pair is contributed
		 */
		boolean contributesPair() {
			return this.contributesPair;
		}

	}

	/**
	 * A member that contributes JSON. Typically, a member will contribute a single
	 * name/value pair based on an extracted value. They may also contribute more complex
	 * JSON structures when configured with one of the {@code using(...)} methods.
	 * <p>
	 * The {@code when(...)} methods may be used to filter a member (omit it entirely from
	 * the JSON). The {@link #as(Extractor)} method can be used to adapt to a different
	 * type.
	 *
	 * @param <T> the member type
	 */
	final class Member<T> {

		private final int index;

		private final @Nullable String name;

		private ValueExtractor<? extends @Nullable T> valueExtractor;

		private @Nullable BiConsumer<T, BiConsumer<?, ?>> pairs;

		private @Nullable Members<T> members;

		Member(int index, @Nullable String name, ValueExtractor<? extends @Nullable T> valueExtractor) {
			this.index = index;
			this.name = name;
			this.valueExtractor = valueExtractor;
		}

		/**
		 * Only include this member when its value is not {@code null}.
		 * @return a {@link Member} which may be configured further
		 */
		public Member<T> whenNotNull() {
			return when(Objects::nonNull);
		}

		/**
		 * Only include this member when an extracted value is not {@code null}.
		 * @param extractor an function used to extract the value to test
		 * @return a {@link Member} which may be configured further
		 */
		public Member<T> whenNotNull(Function<@Nullable T, ?> extractor) {
			Assert.notNull(extractor, "'extractor' must not be null");
			return when((instance) -> Objects.nonNull(extractor.apply(instance)));
		}

		/**
		 * Only include this member when it is not {@code null} and has a
		 * {@link Object#toString() toString()} that is not zero length.
		 * @return a {@link Member} which may be configured further
		 * @see StringUtils#hasLength(CharSequence)
		 */
		public Member<T> whenHasLength() {
			return when((instance) -> instance != null && StringUtils.hasLength(instance.toString()));
		}

		/**
		 * Only include this member when it is not empty (See
		 * {@link ObjectUtils#isEmpty(Object)} for details).
		 * @return a {@link Member} which may be configured further
		 */
		public Member<T> whenNotEmpty() {
			Predicate<@Nullable T> isEmpty = ObjectUtils::isEmpty;
			return whenNot(isEmpty);
		}

		/**
		 * Only include this member when the given predicate does not match.
		 * @param predicate the predicate to test
		 * @return a {@link Member} which may be configured further
		 */
		public Member<T> whenNot(Predicate<@Nullable T> predicate) {
			Assert.notNull(predicate, "'predicate' must not be null");
			return when(predicate.negate());
		}

		/**
		 * Only include this member when the given predicate matches.
		 * @param predicate the predicate to test
		 * @return a {@link Member} which may be configured further
		 */
		public Member<T> when(Predicate<? super @Nullable T> predicate) {
			Assert.notNull(predicate, "'predicate' must not be null");
			this.valueExtractor = this.valueExtractor.when(predicate);
			return this;
		}

		/**
		 * Adapt the value by applying the given {@link Function}.
		 * @param <R> the result type
		 * @param extractor a {@link Extractor} to adapt the value
		 * @return a {@link Member} which may be configured further
		 */
		@SuppressWarnings("unchecked")
		public <R> Member<R> as(Extractor<T, R> extractor) {
			Assert.notNull(extractor, "'adapter' must not be null");
			Member<R> result = (Member<R>) this;
			result.valueExtractor = this.valueExtractor.as(extractor::extract);
			return result;
		}

		/**
		 * Add JSON name/value pairs by extracting values from a series of elements.
		 * Typically used with a {@link Iterable#forEach(Consumer)} call, for example:
		 *
		 * <pre class="code">
		 * members.add(Event::getTags).usingExtractedPairs(Iterable::forEach, pairExtractor);
		 * </pre>
		 * <p>
		 * When used with a named member, the pairs will be added as a new JSON value
		 * object:
		 *
		 * <pre>
		 * {
		 *   "name": {
		 *     "p1": 1,
		 *     "p2": 2
		 *   }
		 * }
		 * </pre>
		 *
		 * When used with an unnamed member the pairs will be added to the existing JSON
		 * object:
		 *
		 * <pre>
		 * {
		 *   "p1": 1,
		 *   "p2": 2
		 * }
		 * </pre>
		 * @param <E> the element type
		 * @param elements callback used to provide the elements
		 * @param extractor a {@link PairExtractor} used to extract the name/value pair
		 * @return a {@link Member} which may be configured further
		 * @see #usingExtractedPairs(BiConsumer, Function, Function)
		 * @see #usingPairs(BiConsumer)
		 */
		public <E> Member<T> usingExtractedPairs(BiConsumer<T, Consumer<E>> elements, PairExtractor<E> extractor) {
			Assert.notNull(elements, "'elements' must not be null");
			Assert.notNull(extractor, "'extractor' must not be null");
			return usingExtractedPairs(elements, extractor::getName, extractor::getValue);
		}

		/**
		 * Add JSON name/value pairs by extracting values from a series of elements.
		 * Typically used with a {@link Iterable#forEach(Consumer)} call, for example:
		 *
		 * <pre class="code">
		 * members.add(Event::getTags).usingExtractedPairs(Iterable::forEach, Tag::getName, Tag::getValue);
		 * </pre>
		 * <p>
		 * When used with a named member, the pairs will be added as a new JSON value
		 * object:
		 *
		 * <pre>
		 * {
		 *   "name": {
		 *     "p1": 1,
		 *     "p2": 2
		 *   }
		 * }
		 * </pre>
		 *
		 * When used with an unnamed member the pairs will be added to the existing JSON
		 * object:
		 *
		 * <pre>
		 * {
		 *   "p1": 1,
		 *   "p2": 2
		 * }
		 * </pre>
		 * @param <E> the element type
		 * @param <N> the name type
		 * @param <V> the value type
		 * @param elements callback used to provide the elements
		 * @param nameExtractor {@link Function} used to extract the name
		 * @param valueExtractor {@link Function} used to extract the value
		 * @return a {@link Member} which may be configured further
		 * @see #usingExtractedPairs(BiConsumer, PairExtractor)
		 * @see #usingPairs(BiConsumer)
		 */
		public <E, N, V> Member<T> usingExtractedPairs(BiConsumer<T, Consumer<E>> elements,
				Function<E, N> nameExtractor, Function<E, V> valueExtractor) {
			Assert.notNull(elements, "'elements' must not be null");
			Assert.notNull(nameExtractor, "'nameExtractor' must not be null");
			Assert.notNull(valueExtractor, "'valueExtractor' must not be null");
			return usingPairs((instance, pairsConsumer) -> elements.accept(instance, (element) -> {
				N name = nameExtractor.apply(element);
				V value = valueExtractor.apply(element);
				pairsConsumer.accept(name, value);
			}));
		}

		/**
		 * Add JSON name/value pairs. Typically used with a
		 * {@link Map#forEach(BiConsumer)} call, for example:
		 *
		 * <pre class="code">
		 * members.add(Event::getLabels).usingPairs(Map::forEach);
		 * </pre>
		 * <p>
		 * When used with a named member, the pairs will be added as a new JSON value
		 * object:
		 *
		 * <pre>
		 * {
		 *   "name": {
		 *     "p1": 1,
		 *     "p2": 2
		 *   }
		 * }
		 * </pre>
		 *
		 * When used with an unnamed member the pairs will be added to the existing JSON
		 * object:
		 *
		 * <pre>
		 * {
		 *   "p1": 1,
		 *   "p2": 2
		 * }
		 * </pre>
		 * @param <N> the name type
		 * @param <V> the value type
		 * @param pairs callback used to provide the pairs
		 * @return a {@link Member} which may be configured further
		 * @see #usingExtractedPairs(BiConsumer, PairExtractor)
		 * @see #usingPairs(BiConsumer)
		 */
		@SuppressWarnings({ "unchecked", "rawtypes" })
		public <N, V> Member<T> usingPairs(BiConsumer<T, BiConsumer<N, V>> pairs) {
			Assert.notNull(pairs, "'pairs' must not be null");
			Assert.state(this.pairs == null, "Pairs cannot be declared multiple times");
			Assert.state(this.members == null, "Pairs cannot be declared when using members");
			this.pairs = (BiConsumer) pairs;
			return this;
		}

		/**
		 * Add JSON based on further {@link Members} configuration. For example:
		 *
		 * <pre class="code">
		 * members.add(User::getName).usingMembers((personMembers) -> {
		 *     personMembers.add("first", Name::first);
		 *     personMembers.add("last", Name::last);
		 * });
		 * </pre>
		 *
		 * <p>
		 * When used with a named member, the result will be added as a new JSON value
		 * object:
		 *
		 * <pre>
		 * {
		 *   "name": {
		 *     "first": "Jane",
		 *     "last": "Doe"
		 *   }
		 * }
		 * </pre>
		 *
		 * When used with an unnamed member the result will be added to the existing JSON
		 * object:
		 *
		 * <pre>
		 * {
		 *   "first": "John",
		 *   "last": "Doe"
		 * }
		 * </pre>
		 * @param members callback to configure the members
		 * @return a {@link Member} which may be configured further
		 * @see #usingExtractedPairs(BiConsumer, PairExtractor)
		 * @see #usingPairs(BiConsumer)
		 */
		public Member<T> usingMembers(Consumer<Members<T>> members) {
			Assert.notNull(members, "'members' must not be null");
			Assert.state(this.members == null, "Members cannot be declared multiple times");
			Assert.state(this.pairs == null, "Members cannot be declared when using pairs");
			this.members = new Members<>(members, this.name == null);
			return this;
		}

		/**
		 * Writes the given instance using details configure by this member.
		 * @param instance the instance to write
		 * @param valueWriter the JSON value writer to use
		 */
		void write(@Nullable Object instance, JsonValueWriter valueWriter) {
			T extracted = this.valueExtractor.extract(instance);
			if (ValueExtractor.skip(extracted)) {
				return;
			}
			Object value = getValueToWrite(extracted, valueWriter);
			valueWriter.write(this.name, value);
		}

		private @Nullable Object getValueToWrite(@Nullable T extracted, JsonValueWriter valueWriter) {
			WritableJson writableJson = getWritableJsonToWrite(extracted, valueWriter);
			return (writableJson != null) ? WritableJson.of(writableJson) : extracted;
		}

		private @Nullable WritableJson getWritableJsonToWrite(@Nullable T extracted, JsonValueWriter valueWriter) {
			BiConsumer<T, BiConsumer<?, ?>> pairs = this.pairs;
			if (pairs != null) {
				return (out) -> valueWriter.writePairs((outPairs) -> pairs.accept(extracted, outPairs));
			}
			Members<T> members = this.members;
			if (members != null) {
				return (out) -> members.write(extracted, valueWriter);
			}
			return null;
		}

		/**
		 * Whether this contributes one or more name/value pairs to the JSON.
		 * @return whether a name/value pair is contributed
		 */
		boolean contributesPair() {
			return this.name != null || this.pairs != null || (this.members != null && this.members.contributesPair());
		}

		@Override
		public String toString() {
			return "Member at index " + this.index + ((this.name != null) ? "{%s}".formatted(this.name) : "");
		}

		/**
		 * Internal class used to manage member value extraction and filtering.
		 *
		 * @param <T> the member type
		 */
		@FunctionalInterface
		interface ValueExtractor<T extends @Nullable Object> {

			/**
			 * Represents a skipped value.
			 */
			Object SKIP = new Object();

			/**
			 * Extract the value from the given instance.
			 * @param instance the source instance
			 * @return the extracted value or {@link #SKIP}
			 */
			@Nullable T extract(@Nullable Object instance);

			/**
			 * Only extract when the given predicate matches.
			 * @param predicate the predicate to test
			 * @return a new {@link ValueExtractor}
			 */
			default ValueExtractor<T> when(Predicate<? super @Nullable T> predicate) {
				return (instance) -> test(extract(instance), predicate);
			}

			@SuppressWarnings("unchecked")
			private @Nullable T test(@Nullable T extracted, Predicate<? super @Nullable T> predicate) {
				return (!skip(extracted) && predicate.test(extracted)) ? extracted : (T) SKIP;
			}

			/**
			 * Adapt the extracted value.
			 * @param <R> the result type
			 * @param extractor the extractor to use
			 * @return a new {@link ValueExtractor}
			 */
			default <R> ValueExtractor<R> as(Extractor<T, R> extractor) {
				return (instance) -> apply(extract(instance), extractor);
			}

			@SuppressWarnings("unchecked")
			private <R> @Nullable R apply(@Nullable T value, Extractor<T, R> extractor) {
				if (skip(value)) {
					return (R) SKIP;
				}
				return (value != null) ? extractor.extract(value) : null;
			}

			/**
			 * Create a new {@link ValueExtractor} based on the given {@link Function}.
			 * @param <S> the source type
			 * @param <T> the extracted type
			 * @param extractor the extractor to use
			 * @return a new {@link ValueExtractor} instance
			 */
			@SuppressWarnings("unchecked")
			static <S, T> ValueExtractor<T> of(Extractor<S, T> extractor) {
				return (instance) -> {
					if (instance == null) {
						return null;
					}
					return (skip(instance)) ? (T) SKIP : extractor.extract((S) instance);
				};
			}

			/**
			 * Return if the extracted value should be skipped.
			 * @param <T> the value type
			 * @param extracted the value to test
			 * @return if the value is to be skipped
			 */
			static <T> boolean skip(@Nullable T extracted) {
				return extracted == SKIP;
			}

		}

	}

	/**
	 * A path used to identify a specific JSON member. Paths can be represented as strings
	 * in form {@code "my.json[1].item"} where elements are separated by {@code '.' } or
	 * {@code [<index>]}. Reserved characters are escaped using {@code '\'}.
	 *
	 * @param parent the parent of this path
	 * @param name the name of the member or {@code null} if the member is indexed. Path
	 * names are provided as they were defined when the member was added and do not
	 * include any {@link NameProcessor name processing}.
	 * @param index the index of the member or {@link MemberPath#UNINDEXED}
	 */
	record MemberPath(@Nullable MemberPath parent, @Nullable String name, int index) {

		private static final String[] ESCAPED = { "\\", ".", "[", "]" };

		public MemberPath {
			Assert.isTrue((name != null && index < 0) || (name == null && index >= 0),
					"'name' and 'index' cannot be mixed");
		}

		/**
		 * Indicates that the member has no index.
		 */
		public static final int UNINDEXED = -1;

		/**
		 * The root of all member paths.
		 */
		static final MemberPath ROOT = new MemberPath(null, "", UNINDEXED);

		/**
		 * Create a new child from this path with the specified index.
		 * @param index the index of the child
		 * @return a new {@link MemberPath} instance
		 */
		public MemberPath child(int index) {
			return new MemberPath(this, null, index);
		}

		/**
		 * Create a new child from this path with the specified name.
		 * @param name the name of the child
		 * @return a new {@link MemberPath} instance
		 */
		public MemberPath child(String name) {
			return (!StringUtils.hasLength(name)) ? this : new MemberPath(this, name, UNINDEXED);
		}

		@Override
		public String toString() {
			return toString(true);
		}

		/**
		 * Return a string representation of the path without any escaping.
		 * @return the unescaped string representation
		 */
		public String toUnescapedString() {
			return toString(false);
		}

		private String toString(boolean escape) {
			StringBuilder string = new StringBuilder((this.parent != null) ? this.parent.toString(escape) : "");
			if (this.index >= 0) {
				string.append("[").append(this.index).append("]");
			}
			else {
				string.append((!string.isEmpty()) ? "." : "").append((!escape) ? this.name : escape(this.name));
			}
			return string.toString();
		}

		private @Nullable String escape(@Nullable String name) {
			if (name == null) {
				return null;
			}
			for (String escape : ESCAPED) {
				name = name.replace(escape, "\\" + escape);
			}
			return name;
		}

		/**
		 * Create a new {@link MemberPath} instance from the given string.
		 * @param value the path value
		 * @return a new {@link MemberPath} instance
		 */
		public static MemberPath of(String value) {
			MemberPath path = MemberPath.ROOT;
			StringBuilder buffer = new StringBuilder();
			boolean escape = false;
			for (char ch : value.toCharArray()) {
				if (!escape && ch == '\\') {
					escape = true;
				}
				else if (!escape && (ch == '.' || ch == '[')) {
					path = path.child(buffer.toString());
					buffer.setLength(0);
				}
				else if (!escape && ch == ']') {
					path = path.child(Integer.parseUnsignedInt(buffer.toString()));
					buffer.setLength(0);
				}
				else {
					buffer.append(ch);
					escape = false;
				}
			}
			path = path.child(buffer.toString());
			return path;
		}

	}

	/**
	 * Interface that can be used to extract name/value pairs from an element.
	 *
	 * @param <E> the element type
	 */
	interface PairExtractor<E> {

		/**
		 * Extract the name.
		 * @param <N> the name type
		 * @param element the source element
		 * @return the extracted name
		 */
		<N> N getName(E element);

		/**
		 * Extract the name.
		 * @param <V> the value type
		 * @param element the source element
		 * @return the extracted value
		 */
		<V> V getValue(E element);

		/**
		 * Factory method to create a {@link PairExtractor} using distinct name and value
		 * extraction functions.
		 * @param <T> the element type
		 * @param nameExtractor the name extractor
		 * @param valueExtractor the value extraction
		 * @return a new {@link PairExtractor} instance
		 */
		static <T> PairExtractor<T> of(Function<T, ?> nameExtractor, Function<T, ?> valueExtractor) {
			Assert.notNull(nameExtractor, "'nameExtractor' must not be null");
			Assert.notNull(valueExtractor, "'valueExtractor' must not be null");
			return new PairExtractor<>() {

				@Override
				@SuppressWarnings("unchecked")
				public <N> N getName(T instance) {
					return (N) nameExtractor.apply(instance);
				}

				@Override
				@SuppressWarnings("unchecked")
				public <V> V getValue(T instance) {
					return (V) valueExtractor.apply(instance);
				}

			};
		}

	}

	/**
	 * Callback interface that can be {@link Members#applyingNameProcessor(NameProcessor)
	 * applied} to {@link Members} to change names or filter members.
	 */
	@FunctionalInterface
	interface NameProcessor {

		/**
		 * Return a new name for the JSON member or {@code null} if the member should be
		 * filtered entirely.
		 * @param path the path of the member
		 * @param existingName the existing and possibly already processed name.
		 * @return the new name
		 */
		@Nullable String processName(MemberPath path, String existingName);

		/**
		 * Factory method to create a new {@link NameProcessor} for the given operation.
		 * @param operation the operation to apply
		 * @return a new {@link NameProcessor} instance
		 */
		static NameProcessor of(UnaryOperator<String> operation) {
			Assert.notNull(operation, "'operation' must not be null");
			return (path, existingName) -> operation.apply(existingName);
		}

	}

	/**
	 * Callback interface that can be
	 * {@link Members#applyingValueProcessor(ValueProcessor) applied} to {@link Members}
	 * to process values before they are written. Typically used to filter values, for
	 * example to reduce superfluous information or sanitize sensitive data.
	 *
	 * @param <T> the value type
	 */
	@FunctionalInterface
	interface ValueProcessor<T extends @Nullable Object> {

		/**
		 * Process the value at the given path.
		 * @param path the path of the member containing the value
		 * @param value the value being written (may be {@code null})
		 * @return the processed value
		 */
		@Nullable T processValue(MemberPath path, @Nullable T value);

		/**
		 * Return a new processor from this one that only applied to members with the
		 * given path (ignoring escape characters).
		 * @param path the patch to match
		 * @return a new {@link ValueProcessor} that only applies when the path matches
		 */
		default ValueProcessor<T> whenHasUnescapedPath(String path) {
			return whenHasPath((candidate) -> candidate.toString(false).equals(path));
		}

		/**
		 * Return a new processor from this one that only applied to members with the
		 * given path.
		 * @param path the patch to match
		 * @return a new {@link ValueProcessor} that only applies when the path matches
		 */
		default ValueProcessor<T> whenHasPath(String path) {
			return whenHasPath(MemberPath.of(path)::equals);
		}

		/**
		 * Return a new processor from this one that only applied to members that match
		 * the given path predicate.
		 * @param predicate the predicate that must match
		 * @return a new {@link ValueProcessor} that only applies when the predicate
		 * matches
		 */
		default ValueProcessor<T> whenHasPath(Predicate<MemberPath> predicate) {
			return (path, value) -> (predicate.test(path)) ? processValue(path, value) : value;
		}

		/**
		 * Return a new processor from this one that only applies to member with values of
		 * the given type.
		 * @param type the type that must match
		 * @return a new {@link ValueProcessor} that only applies when value is the given
		 * type.
		 */
		default ValueProcessor<T> whenInstanceOf(Class<?> type) {
			Predicate<@Nullable T> isInstance = type::isInstance;
			return when(isInstance);
		}

		/**
		 * Return a new processor from this one that only applies to member with values
		 * that match the given predicate.
		 * @param predicate the predicate that must match
		 * @return a new {@link ValueProcessor} that only applies when the predicate
		 * matches
		 */
		default ValueProcessor<T> when(Predicate<@Nullable T> predicate) {
			return (name, value) -> (predicate.test(value)) ? processValue(name, value) : value;
		}

		/**
		 * Factory method to crate a new {@link ValueProcessor} that applies the given
		 * action.
		 * @param <T> the value type
		 * @param type the value type
		 * @param action the action to apply
		 * @return a new {@link ValueProcessor} instance
		 */
		static <T> ValueProcessor<T> of(Class<? extends T> type, UnaryOperator<@Nullable T> action) {
			return of(action).whenInstanceOf(type);
		}

		/**
		 * Factory method to crate a new {@link ValueProcessor} that applies the given
		 * action.
		 * @param <T> the value type
		 * @param action the action to apply
		 * @return a new {@link ValueProcessor} instance
		 */
		static <T> ValueProcessor<T> of(UnaryOperator<@Nullable T> action) {
			Assert.notNull(action, "'action' must not be null");
			return (name, value) -> action.apply(value);
		}

	}

	/**
	 * Interface that can be used to extract one value from another.
	 *
	 * @param <T> the source type
	 * @param <R> the result type
	 */
	@FunctionalInterface
	interface Extractor<T extends @Nullable Object, R extends @Nullable Object> {

		/**
		 * Extract from the given value.
		 * @param value the source value (never {@code null})
		 * @return an extracted value or {@code null}
		 */
		@Nullable R extract(@NonNull T value);

	}

}

Analyze Your Own Codebase

Get architecture documentation, dependency graphs, and domain analysis for your codebase in minutes.

Try Supermodel Free