FieldName.java

/*
 * Copyright 2023-present the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.mongodb.core.mapping;

import org.springframework.util.ObjectUtils;

/**
 * Value Object representing a field name that should be used to read/write fields within the MongoDB document.
 * {@link FieldName Field names} field names may contain special characters (such as dot ({@literal .})) but may be
 * treated differently depending on their {@link Type type}.
 *
 * @author Christoph Strobl
 * @since 4.2
 */
public record FieldName(String name, Type type, String[] parts) {

	public FieldName(String name, Type type) {
		this(name, type, name.split("\\."));
	}

	private static final String ID_KEY = "_id";

	public static final FieldName ID = new FieldName(ID_KEY, Type.KEY);

	/**
	 * Create a new {@link FieldName} that treats the given {@literal value} as is.
	 *
	 * @param value must not be {@literal null}.
	 * @return new instance of {@link FieldName}.
	 */
	public static FieldName name(String value) {
		return new FieldName(value, Type.KEY);
	}

	/**
	 * Create a new {@link FieldName} that treats the given {@literal value} as a path. If the {@literal value} contains
	 * dot ({@literal .}) characters, they are considered deliminators in a path.
	 *
	 * @param value must not be {@literal null}.
	 * @return new instance of {@link FieldName}.
	 */
	public static FieldName path(String value) {
		return new FieldName(value, Type.PATH);
	}

	/**
	 * Get the parts the field name consists of. If the {@link FieldName} is a {@link Type#KEY} or a {@link Type#PATH}
	 * that does not contain dot ({@literal .}) characters an array containing a single element is returned. Otherwise the
	 * {@link #name()} is split into segments using dot ({@literal .}) as a separator.
	 *
	 * @return never {@literal null}.
	 */
	public String[] parts() {

		if (isKey()) {
			return new String[] { name };
		}

		return parts;
	}

	/**
	 * @param type return true if the given {@link Type} is equal to {@link #type()}.
	 * @return {@literal true} if values are equal.
	 */
	public boolean isOfType(Type type) {
		return ObjectUtils.nullSafeEquals(type(), type);
	}

	/**
	 * @return whether the field name represents a key (i.e. as-is name).
	 */
	public boolean isKey() {
		return isOfType(Type.KEY);
	}

	/**
	 * @return whether the field name represents a path (i.e. dot-path).
	 */
	public boolean isPath() {
		return isOfType(Type.PATH);
	}

	@Override
	public String toString() {
		return "FieldName{%s=%s}".formatted(isKey() ? "key" : "path", name);
	}

	@Override
	public boolean equals(Object o) {

		if (o == this) {
			return true;
		}
		if (o == null || getClass() != o.getClass()) {
			return false;
		}
		FieldName fieldName = (FieldName) o;
		return ObjectUtils.nullSafeEquals(name, fieldName.name) && type == fieldName.type;
	}

	@Override
	public int hashCode() {

		int hashCode = ObjectUtils.nullSafeHashCode(name);
		return 31 * hashCode + ObjectUtils.nullSafeHashCode(type);
	}

	/**
	 * The {@link FieldName.Type type} defines how to treat a {@link FieldName} that contains special characters.
	 *
	 * @author Christoph Strobl
	 * @since 4.2
	 */
	public enum Type {

		/**
		 * Dot ({@literal .}) characters are treated as separators for segments in a path.
		 */
		PATH,

		/**
		 * Values are used as is.
		 */
		KEY
	}
}