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

io.jstach.kiwi.kvs.DefaultKeyValuesResourceParser Maven / Gradle / Ivy

package io.jstach.kiwi.kvs;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.stream.Stream;

import org.jspecify.annotations.Nullable;

import io.jstach.kiwi.kvs.KeyValuesServiceProvider.KeyValuesFilter.Filter;

/*
 * The idea here is to keep all the parsing logic separated from the core domain so that
 * we can switch the key value scheme of loading.
 */
enum DefaultKeyValuesResourceParser implements KeyValuesResourceParser {

	DEFAULT {

	};

	// Formatting resource

	@Override
	public void formatResource(KeyValuesResource _resource, BiConsumer consumer) {

		InternalKeyValuesResource resource = switch (_resource) {
			case InternalKeyValuesResource ir -> ir;
		};

		for (var rk : ResourceKey.values()) {
			String resourceName = resource.name();
			var uri = resource.uri();
			String alias = rk.formatAlias();
			switch (rk) {
				case LOAD -> {
					var key = externalResourceKey1(rk, alias, resourceName);
					consumer.accept(key, uri.toString());
				}
				case FLAGS -> {
					var flags = resource.loadFlags();
					var key = externalResourceKey1(rk, alias, resourceName);
					if (!flags.isEmpty()) {
						var flagsCsv = LoadFlag.toCSV(flags.stream());
						consumer.accept(key, flagsCsv);
					}
				}
				case MEDIA_TYPE -> {
					var key = externalResourceKey1(rk, alias, resourceName);
					String mediaType = resource.mediaType();
					if (mediaType != null) {
						consumer.accept(key, mediaType);
					}
				}
				case PARAM -> {
					// var key = externalResourceKey2Prefix(rk, resourceName);
					resource.parameters().forKeyValues((k, v) -> {
						var key = formatParameterKey(resource, k);
						consumer.accept(key, v);
					});
				}
				case FILTER -> {
					var filters = resource.filters();
					for (var f : filters) {
						var key = formatFilterKey(resource, f);
						var value = f.expression();
						consumer.accept(key, value);
					}
				}
			}
		}
	}

	@Override
	public String formatParameterKey(KeyValuesResource resource, String parameterName) {
		String alias = ResourceKey.PARAM.formatAlias();
		return externalResourceKey2Prefix(ResourceKey.PARAM, alias, resource.name()) + parameterName;
	}

	// Filtering resource

	@Override
	public KeyValues filterResources(KeyValues keyValues) {
		return keyValues.filter(this::filter);
	}

	private boolean filter(KeyValue kv) {
		try {
			return resourceKeyOrNull(kv) == null;
		}
		catch (KeyValuesResourceParserException e) {
			return true;
		}
	}

	// Parsing Resource

	@Override
	public List parseResources(KeyValues keyValues)
			throws KeyValuesResourceParserException {
		List resources = new ArrayList<>();
		for (var kv : keyValues) {
			var r = parseResourceOrNull(kv, keyValues);
			if (r != null) {
				resources.add(r);
			}
		}
		return List.copyOf(resources);
	}

	// Parsing Resource

	private void parseKeyValues(KeyValuesResource.Builder builder, LoadResource resource, KeyValues keyValues)
			throws KeyValuesResourceParserException {
		for (var kv : keyValues) {
			var rkv = resourceKeyOrNull(kv);
			if (rkv != null) {
				if (resource.name().equals(rkv.resourceName())) {
					parseResourceKey(builder, rkv);
				}
			}
		}
	}

	private void parseResourceKey(KeyValuesResource.Builder builder, ResourceKeyValue rkv)
			throws KeyValuesResourceParserException {
		var type = rkv.type();
		var kv = rkv.keyValue();
		switch (type) {
			case LOAD -> {
			}
			case MEDIA_TYPE -> {
				builder.mediaType(kv.expanded());
			}
			case FLAGS -> {
				builder._flags(LoadFlag.parseCSV(kv.expanded()));
			}
			case PARAM -> {
				String name = rkv.paramName();
				if (name == null) {
					throw new IllegalStateException("bug");
				}
				builder.parameter(name, kv.expanded());
			}
			case FILTER -> {
				String filter = rkv.paramName();
				if (filter == null) {
					throw new IllegalStateException("bug");
				}
				String expression = kv.value();
				var f = new Filter(filter, expression, "");
				builder._addFilter(f);
			}
		}
	}

	private @Nullable InternalKeyValuesResource parseResourceOrNull(KeyValue keyValue, KeyValues keyValues)
			throws KeyValuesResourceParserException {
		var resource = parseLoadOrNull(keyValue);
		if (resource == null) {
			return null;
		}
		var builder = new KeyValuesResource.Builder(resource.uri(), resource.name());
		parseURI(builder, resource.uri());
		parseKeyValues(builder, resource, keyValues);
		builder.reference = keyValue;
		return builder.buildNormalized();
	}

	@Override
	public InternalKeyValuesResource normalizeResource(KeyValuesResource resource)
			throws KeyValuesResourceParserException {
		InternalKeyValuesResource r = switch (resource) {
			case InternalKeyValuesResource ir -> ir;
		};
		if (r.normalized()) {
			return r;
		}
		var builder = KeyValuesResource.builder(r.uri()).name(r.name());
		builder.reference = r.reference();
		parseURI(builder, r.uri());
		for (var rk : ResourceKey.values()) {
			switch (rk) {
				case LOAD -> {
				}
				case MEDIA_TYPE -> {
					switch (r.mediaType()) {
						case String s -> builder.mediaType(s);
						case null -> {
						}
					}
				}
				case FLAGS -> {
					for (var f : r.loadFlags()) {
						builder._addFlag(f);
					}
				}
				case PARAM -> {
					r.parameters().forKeyValues((k, v) -> {
						builder.parameter(k, v);
					});
				}
				case FILTER -> {
					for (var f : r.filters()) {
						builder._addFilter(f);
					}
				}
			}
		}
		return builder.buildNormalized();
	}

	private @Nullable LoadResource parseLoadOrNull(KeyValue keyValue) throws KeyValuesResourceParserException {
		String key = keyValue.key();
		String prefix = prefix(ResourceKey.LOAD.keys().getFirst());
		if (!key.startsWith(prefix)) {
			return null;
		}
		String name = key.substring(prefix.length());

		try {
			name = KeyValuesSource.validateName(name);
		}
		catch (IllegalArgumentException e) {
			throw new KeyValuesResourceParserException(
					"Key Value has a malformed name for load resource. keyValue=" + keyValue, e);
		}
		var uri = URI.create(keyValue.expanded());
		return new LoadResource(name, uri, keyValue);
	}

	private void parseURI(KeyValuesResource.Builder builder, URI uri) throws KeyValuesResourceParserException {
		if (uri.getQuery() == null) {
			return;
		}
		var uriParameters = DefaultKeyValuesMedia.parseURI(uri);
		String resourceName = builder.name;
		List filtered = new ArrayList<>();
		boolean hasParameter = false;
		for (var kv : uriParameters) {
			ResourceKeyValue rk;
			try {
				rk = uriResourceKeyOrNull(kv, resourceName);
			}
			catch (KeyValuesResourceParserException e) {
				throw new KeyValuesResourceParserException("URI query parameter resource key invalid in uri: " + uri,
						e);
			}
			if (rk == null) {
				continue;
			}
			hasParameter = true;
			switch (rk.type()) {
				case LOAD -> {
				}
				case MEDIA_TYPE -> {
					builder.mediaType(kv.value());
				}
				case FLAGS -> {
					builder.addFlags(kv.value());
				}
				case PARAM -> {
					String param = rk.paramName();
					if (param == null) {
						throw new IllegalStateException("bug");
					}
					String value = kv.value();
					builder.parameter(param, value);
				}
				case FILTER -> {
					String filter = rk.paramName();
					if (filter == null) {
						throw new IllegalStateException("bug");
					}
					String expression = kv.value();
					var f = new Filter(filter, expression, "");
					builder._addFilter(f);
				}
				case null -> {
					filtered.add(kv);
				}
			}
		}
		if (hasParameter) {
			String uriNoQuery = uriWithoutQuery(uri);
			if (filtered.isEmpty()) {
				builder.uri = URI.create(uriNoQuery);
			}
			else {
				KeyValues queryKvs = () -> filtered.stream();
				builder.uri = URI.create(uriNoQuery + "?" + KeyValuesMedia.ofUrlEncoded().formatter().format(queryKvs));
			}
		}

	}

	// TODO consider putting this in KeyValuesMedia if we need this again.
	private static String uriWithoutQuery(URI uri) {
		String s = uri.toString();
		if (uri.getQuery() == null) {
			return s;
		}
		int index = s.indexOf('?');
		return s.substring(0, index);
	}

	protected String prefix() {
		return sep();
	}

	protected String sep() {
		return "_";
	}

	protected String prefix(String keyAlias) {
		return prefix() + keyAlias + sep();
	}

	private String externalResourceKey1(ResourceKey type, String alias, String resourceName) {
		return switch (type) {
			case LOAD, FLAGS, MEDIA_TYPE -> prefix(alias) + resourceName;
			default -> throw new IllegalArgumentException("key: " + type);

		};
	}

	private String externalResourceKey2Prefix(ResourceKey type, String alias, String resourceName) {
		return switch (type) {
			case FILTER, PARAM -> prefix(alias) + resourceName + sep();
			default -> throw new IllegalArgumentException("key: " + type);

		};
	}

	protected @Nullable ResourceKeyValue uriResourceKeyOrNull(KeyValue kv, String resourceName)
			throws KeyValuesResourceParserException {
		for (var rk : ResourceKey.values()) {
			for (String alias : rk.keys()) {
				ResourceKeyValue r = uriResourceKeyOrNull(kv, resourceName, rk, alias);
				if (r != null) {
					return r;
				}
			}
		}
		return null;
	}

	private @Nullable ResourceKeyValue uriResourceKeyOrNull(KeyValue kv, String resourceName, ResourceKey rk,
			String alias) throws KeyValuesResourceParserException {
		String key = kv.key();
		String prefix = prefix() + alias;
		if (!key.startsWith(prefix)) {
			return null;
		}
		String rest = key.substring(prefix.length());
		ResourceKeyValue r = switch (rk) {
			case LOAD, MEDIA_TYPE, FLAGS -> {
				if (key.equals(prefix)) {
					yield new ResourceKeyValue(rk, alias, kv, resourceName, null);
				}
				if (rest.startsWith(sep())) {
					throw new KeyValuesResourceParserException("Bad resource key. type: " + rk + " keyvalue: " + kv);
				}
				yield null;
			}
			case PARAM, FILTER -> {
				if (key.equals(prefix)) {
					throw new KeyValuesResourceParserException("Bad resource key. type: " + rk + " keyvalue: " + kv);
				}
				if (!rest.startsWith(sep())) {
					yield null;
				}
				String paramName = rest.substring(sep().length());
				if (paramName.isBlank()) {
					throw new KeyValuesResourceParserException("Bad resource key. type: " + rk + " keyvalue: " + kv);
				}
				yield new ResourceKeyValue(rk, alias, kv, resourceName, paramName);
			}
		};
		return r;
	}

	protected @Nullable ResourceKeyValue resourceKeyOrNull(KeyValue kv) throws KeyValuesResourceParserException {
		for (var type : ResourceKey.values()) {
			for (String alias : type.keys()) {
				ResourceKeyValue r = resourceKeyOrNull(kv, type, alias);
				if (r != null) {
					return r;
				}
			}
		}
		return null;
	}

	private @Nullable ResourceKeyValue resourceKeyOrNull(KeyValue kv, ResourceKey type, String alias)
			throws KeyValuesResourceParserException {
		String prefix = prefix(alias);
		var key = kv.key();
		if (key.equals(prefix)) {
			throw new KeyValuesResourceParserException("Bad resource key. type: " + type + " keyvalue: " + kv);
		}
		if (!key.startsWith(prefix)) {
			return null;
		}
		String rest = key.substring(prefix.length());
		String[] names = rest.split(sep(), 2);
		if (names.length == 0) {
			throw new KeyValuesResourceParserException("Bad resource key. type: " + type + " keyvalue: " + kv);
		}
		String resourceName = names[0];
		try {
			resourceName = KeyValuesSource.validateName(resourceName);
		}
		catch (IllegalArgumentException e) {
			throw new KeyValuesResourceParserException(
					"Key Value has a malformed name for load resource. keyValue=" + kv, e);
		}
		return switch (type) {
			case LOAD, MEDIA_TYPE, FLAGS -> {
				if (names.length == 1) {
					yield new ResourceKeyValue(type, alias, kv, resourceName, null);
				}
				throw new KeyValuesResourceParserException("Bad resource key. type: " + type + " keyvalue: " + kv);
			}
			case PARAM, FILTER -> {
				if (names.length == 2) {
					yield new ResourceKeyValue(type, alias, kv, resourceName, names[1]);
				}
				throw new KeyValuesResourceParserException("Bad resource key. type: " + type + " keyvalue: " + kv);
			}
		};
	}

	protected String formatFilterKey(KeyValuesResource resource, Filter filter) {
		String alias = ResourceKey.FILTER.formatAlias();
		String prefix = externalResourceKey2Prefix(ResourceKey.FILTER, alias, resource.name());
		String key = prefix + filter.filter();
		String name = filter.name();
		if (!name.isBlank()) {
			key = key + sep() + name;
		}
		return key;

	}

	public static DefaultKeyValuesResourceParser of() {
		return DEFAULT;
	}

	private record ResourceKeyValue(ResourceKey type, String alias, KeyValue keyValue, String resourceName,
			@Nullable String paramName) {

	}

	private record LoadResource(String name, URI uri, KeyValue keyValue) {
	}

	@SuppressWarnings({ "ImmutableEnumChecker" })
	private enum ResourceKey {

		LOAD(KeyValuesResource.KEY_LOAD), //
		FLAGS(KeyValuesResource.KEY_FLAGS, KeyValuesResource.KEY_FLAG), //
		MEDIA_TYPE(KeyValuesResource.KEY_MEDIA_TYPE, KeyValuesResource.KEY_MIME), //
		PARAM(KeyValuesResource.KEY_PARAM, KeyValuesResource.KEY_PARM), //
		FILTER(KeyValuesResource.KEY_FILTER, KeyValuesResource.KEY_FILT);

		final List keys;

		private ResourceKey(String key, String... rest) {
			this(Stream.concat(Stream.of(key), Stream.of(rest)).toList());
		}

		private ResourceKey(String key) {
			this(List.of(key));
		}

		private ResourceKey(List keys) {
			this.keys = keys;
		}

		String formatAlias() {
			return keys().getFirst();
		}

		List keys() {
			return this.keys;
		}

	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy