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

dev.jbang.catalog.Catalog Maven / Gradle / Ivy

There is a newer version: 0.121.0
Show newest version
package dev.jbang.catalog;

import static dev.jbang.cli.BaseCommand.EXIT_INVALID_INPUT;
import static dev.jbang.cli.BaseCommand.EXIT_UNEXPECTED_STATE;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;

import dev.jbang.Settings;
import dev.jbang.cli.ExitException;
import dev.jbang.source.ResourceRef;
import dev.jbang.util.Util;

public class Catalog {
	public static final String JBANG_CATALOG_JSON = "jbang-catalog.json";
	public static final String JBANG_IMPLICIT_CATALOG_JSON = "implicit-catalog.json";

	static final Map catalogCache = new HashMap<>();

	static final String JBANG_CATALOG_REPO = "jbang-catalog";

	// HEAD at least on github gives you latest commit on default branch
	static final String DEFAULT_REF = "HEAD";

	private static final String CACHE_BUILTIN = ":::BUILTIN:::";

	public Map catalogs = new HashMap<>();
	public Map aliases = new HashMap<>();
	public Map templates = new HashMap<>();

	@SerializedName(value = "base-ref", alternate = { "baseRef" })
	public final String baseRef;
	public final String description;
	public transient ResourceRef catalogRef;

	public Catalog(String baseRef, String description, ResourceRef catalogRef, Map catalogs,
			Map aliases, Map templates) {
		this.baseRef = baseRef;
		this.description = description;
		this.catalogRef = catalogRef;
		catalogs.forEach((key, c) -> this.catalogs.put(key,
				new CatalogRef(c.catalogRef, c.description, c.importItems, this)));
		aliases.forEach((key, a) -> this.aliases.put(key, a.withCatalog(this)));
		templates.forEach((key, t) -> this.templates.put(key,
				new Template(t.fileRefs, t.description, t.properties, this)));
	}

	public static Catalog empty() {
		return new Catalog(null, null, null, Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap());
	}

	/**
	 * Returns in all cases the absolute base reference that can be used to resolve
	 * an Alias' script location. The result will either be a URL or an absolute
	 * path.
	 *
	 * @return A string to be used as the base for Alias script locations
	 */
	public String getScriptBase() {
		if (catalogRef.isClasspath()) {
			return "classpath:" + ((baseRef != null) ? "/" + baseRef : "");
		}
		Path result;
		Path catFile = catalogRef.getFile();
		if (baseRef != null) {
			if (!Util.isRemoteRef(baseRef)) {
				Path base = Paths.get(baseRef);
				if (!base.isAbsolute()) {
					result = catFile.getParent().resolve(base);
				} else {
					result = Paths.get(baseRef);
				}
			} else {
				if (baseRef.endsWith("/")) {
					return baseRef.substring(0, baseRef.length() - 1);
				} else {
					return baseRef;
				}
			}
		} else {
			result = catFile.getParent();
		}
		return result.normalize().toString();
	}

	String relativize(String scriptRef) {
		if (!Util.isRemoteRef(scriptRef) && !isValidCatalogReference(scriptRef)) {
			// If the scriptRef points to an existing file on the local filesystem
			// or it's obviously a path (but not an absolute path) we'll make it
			// relative to the location of the catalog we're adding the alias to.
			Path cwd = Util.getCwd();
			Path script = cwd.resolve(scriptRef).normalize();
			if (script.startsWith(cwd.normalize())) {
				scriptRef = cwd.relativize(script).toString();
			}
			String baseRef = getScriptBase();
			if (!Util.isAbsoluteRef(scriptRef)
					&& !Util.isRemoteRef(baseRef)
					&& (!isValidName(scriptRef) || Files.isRegularFile(script))) {
				Path base = Paths.get(baseRef);
				if (base.getRoot().equals(script.getRoot())) {
					scriptRef = base.relativize(script.toAbsolutePath()).normalize().toString();
				} else {
					scriptRef = script.toAbsolutePath().normalize().toString();
				}
			}
			if (!Util.isRemoteRef(baseRef)
					&& !isValidName(scriptRef)
					&& !Files.isRegularFile(script)) {
				throw new IllegalArgumentException("Source file not found: " + scriptRef);
			}
		}
		return scriptRef;
	}

	void write() throws IOException {
		write(catalogRef.getFile(), this);
	}

	/**
	 * Load a Catalog given the name of a previously registered Catalog
	 *
	 * @param catalogName The name of a registered
	 * @return An Aliases object
	 */
	public static Catalog getByName(String catalogName) {
		CatalogRef catalogRef = CatalogRef.get(simplifyRef(catalogName));
		if (catalogRef != null) {
			return getByRef(catalogRef.catalogRef);
		} else {
			throw new ExitException(EXIT_INVALID_INPUT, "Unknown catalog '" + catalogName + "'");
		}
	}

	/**
	 * Will either return the given catalog or search for the nearest catalog
	 * starting from cwd.
	 *
	 * @param catFile The catalog to return or null to return the nearest catalog
	 * @return Path to a catalog
	 */
	public static Path getCatalogFile(Path catFile) {
		if (catFile == null) {
			Catalog catalog = findNearestCatalog(Util.getCwd());
			if (catalog != null && !catalog.catalogRef.isClasspath()) {
				catFile = catalog.catalogRef.getFile();
			} else {
				// This is here as a backup for when the user catalog doesn't
				// exist yet, because `findNearestCatalog()` only returns
				// existing files
				catFile = Settings.getUserCatalogFile();
			}
		}
		return catFile;
	}

	/**
	 * Load a Catalog's aliases given a file path or URL
	 *
	 * @param catalogRef File path or URL to a Catalog JSON file. If this does not
	 *                   end in .json then jbang-catalog.json will be appended to
	 *                   the end.
	 * @return A Catalog object
	 */
	public static Catalog getByRef(String catalogRef) {
		if (!catalogRef.endsWith(".json")) {
			if (!catalogRef.endsWith("/")) {
				catalogRef += "/";
			}
			catalogRef += JBANG_CATALOG_JSON;
		}
		Path catalogPath = null;
		try {
			Catalog catalog = get(ResourceRef.forResource(catalogRef));
			if (catalog == null) {
				throw new ExitException(EXIT_UNEXPECTED_STATE,
						"Unable to download catalog: " + catalogRef);
			}
			Util.verboseMsg(String.format("Obtained catalog from %s", catalogRef));
			int p = catalogRef.lastIndexOf('/');
			if (p > 0) {
				String baseRef = catalog.baseRef;
				String catalogBaseRef = catalogRef.substring(0, p);
				if (baseRef != null) {
					if (!baseRef.startsWith("/") && !baseRef.contains(":")) {
						baseRef = catalogBaseRef + "/" + baseRef;
					}
				} else {
					baseRef = catalogBaseRef;
				}
				catalog = new Catalog(baseRef, catalog.description, catalog.catalogRef, catalog.catalogs,
						catalog.aliases, catalog.templates);
			}
			return catalog;
		} catch (JsonParseException ex) {
			throw new ExitException(EXIT_UNEXPECTED_STATE,
					"Unable to download catalog: " + catalogRef + " via " + catalogPath, ex);
		}
	}

	/**
	 * Returns a Catalog containing all the aliases from local catalog files merged
	 * into one. This follows the system where aliases that are "nearest" have
	 * priority.
	 *
	 * @param includeImplicits Determines if the implicit catalogs should be merged
	 *                         or not
	 * @return a Catalog object
	 */
	public static Catalog getMerged(boolean includeImported, boolean includeImplicits) {
		List catalogs = new ArrayList<>();
		findNearestCatalogWith(Util.getCwd(), includeImported, includeImplicits, cat -> {
			catalogs.add(0, cat);
			return null;
		});

		Catalog result = Catalog.empty();
		for (Catalog catalog : catalogs) {
			result.aliases.putAll(catalog.aliases);
			result.templates.putAll(catalog.templates);
			result.catalogs.putAll(catalog.catalogs);
		}

		return result;
	}

	private static Catalog findNearestCatalog(Path dir) {
		Path catalogFile = Util.findNearestWith(dir, JBANG_CATALOG_JSON, p -> p);
		return catalogFile != null ? get(catalogFile) : null;
	}

	static Catalog findNearestCatalogWith(Path dir, boolean includeImported, boolean includeImplicits,
			Function accept) {
		Catalog catalog = Util.findNearestWith(dir, JBANG_CATALOG_JSON, p -> {
			try {
				Catalog cat = get(p);
				return accept.apply(cat);
			} catch (Exception e) {
				Util.warnMsg("Unable to read catalog " + p + " because " + e);
				return null;
			}
		});
		if (catalog == null && includeImported) {
			catalog = Util.findNearestWith(dir, JBANG_CATALOG_JSON, p -> {
				try {
					Catalog cat = get(p);
					return findImportedCatalogsWith(cat, accept);
				} catch (Exception e) {
					Util.warnMsg("Unable to read catalog " + p + " because " + e);
					return null;
				}
			});
		}
		if (catalog == null && includeImplicits) {
			Path file = Settings.getUserImplicitCatalogFile();
			if (Files.isRegularFile(file) && Files.isReadable(file)) {
				try {
					Catalog cat = get(file);
					catalog = accept.apply(cat);
				} catch (Exception e) {
					Util.warnMsg("Unable to read catalog " + file + " because " + e);
					return null;
				}
			}
		}
		if (catalog == null) {
			catalog = accept.apply(getBuiltin());
			if (catalog == null && includeImported) {
				catalog = findImportedCatalogsWith(getBuiltin(), accept);
			}
		}
		return catalog;
	}

	static Catalog findImportedCatalogsWith(Catalog catalog, Function accept) {
		for (CatalogRef cr : catalog.catalogs.values()) {
			if (cr.importItems == Boolean.TRUE) {
				try {
					Catalog cat = Catalog.getByRef(cr.catalogRef);
					Catalog result = accept.apply(cat);
					if (result != null)
						return result;
				} catch (Exception e) {
					Util.verboseMsg("Unable to read catalog " + cr.catalogRef + " because " + e);
				}
			}
		}
		return null;
	}

	public static Catalog get(Path catalogPath) {
		if (Files.isDirectory(catalogPath)) {
			catalogPath = catalogPath.resolve(Catalog.JBANG_CATALOG_JSON);
		}
		return get(ResourceRef.forFile(catalogPath));
	}

	private static Catalog get(ResourceRef ref) {
		Catalog catalog;
		Path catalogPath = ref.getFile();
		if (Util.isFresh() || !catalogCache.containsKey(catalogPath.toString())) {
			catalog = read(ref);
			catalog.catalogRef = ref;
			catalogCache.put(catalogPath.toString(), catalog);
		} else {
			catalog = catalogCache.get(catalogPath.toString());
		}
		return catalog;
	}

	// This returns the built-in Catalog that can be found in the resources
	public static Catalog getBuiltin() {
		Catalog catalog = Catalog.empty();
		if (Util.isFresh() || !catalogCache.containsKey(CACHE_BUILTIN)) {
			String res = "classpath:/" + JBANG_CATALOG_JSON;
			ResourceRef catRef = ResourceRef.forResource(res);
			if (catRef != null) {
				catalog = read(catRef);
				catalog.catalogRef = catRef;
				catalogCache.put(CACHE_BUILTIN, catalog);
			}
		} else {
			catalog = catalogCache.get(CACHE_BUILTIN);
		}
		return catalog;
	}

	public static void clearCache() {
		catalogCache.clear();
	}

	static Catalog read(ResourceRef catalogRef) {
		Util.verboseMsg(String.format("Reading catalog from %s", catalogRef.getOriginalResource()));
		Catalog catalog = Catalog.empty();
		if (catalogRef.exists()) {
			try (InputStream is = catalogRef.getInputStream()) {
				catalog = read(is);
			} catch (IOException e) {
				// Ignore errors
			}
		}
		return catalog;
	}

	private static Catalog read(InputStream is) {
		Gson parser = new Gson();
		Catalog catalog = parser.fromJson(new InputStreamReader(is), Catalog.class);
		if (catalog != null) {
			// Validate the result (Gson can't do this)
			if (catalog.catalogs == null) {
				catalog.catalogs = new HashMap<>();
			}
			if (catalog.aliases == null) {
				catalog.aliases = new HashMap<>();
			}
			if (catalog.templates == null) {
				catalog.templates = new HashMap<>();
			}
			for (String catName : catalog.catalogs.keySet()) {
				CatalogRef cat = catalog.catalogs.get(catName);
				cat.catalog = catalog;
				check(cat.catalogRef != null, "Missing required attribute 'catalogs.catalogRef'");
			}
			for (String aliasName : catalog.aliases.keySet()) {
				Alias alias = catalog.aliases.get(aliasName);
				alias.catalog = catalog;
				check(alias.scriptRef != null, "Missing required attribute 'aliases.script-ref'");
			}
			for (String tplName : catalog.templates.keySet()) {
				Template tpl = catalog.templates.get(tplName);
				tpl.catalog = catalog;
				check(tpl.fileRefs != null, "Missing required attribute 'templates.file-refs'");
				check(!tpl.fileRefs.isEmpty(), "Attribute 'templates.file-refs' has no elements");
			}
		} else {
			catalog = Catalog.empty();
		}
		return catalog;
	}

	static void write(Path catalogPath, Catalog catalog) throws IOException {
		try (Writer out = Files.newBufferedWriter(catalogPath)) {
			Gson parser = new GsonBuilder().setPrettyPrinting().create();
			parser.toJson(catalog, out);
		}
	}

	static void check(boolean ok, String message) {
		if (!ok) {
			throw new JsonParseException(message);
		}
	}

	public static String simplifyRef(String catalogRefString) {
		if (Util.isURL(catalogRefString)) {
			ImplicitCatalogRef ref = ImplicitCatalogRef.extract(catalogRefString);
			if (ref != null) {
				return ref.toString();
			}
		} else if (!isValidCatalogReference(catalogRefString)) {
			if (catalogRefString.endsWith("/" + JBANG_CATALOG_REPO)) {
				return catalogRefString.substring(0, catalogRefString.length() - 14);
			} else {
				return catalogRefString.replace("/" + JBANG_CATALOG_REPO + "~", "~");
			}
		}
		return catalogRefString;
	}

	public static boolean isValidName(String name) {
		return name.matches("^[a-zA-Z][-\\w]*$");
	}

	public static boolean isValidCatalogReference(String name) {
		String[] parts = name.split("@");
		if (parts.length < 2) {
			return false;
		}
		for (String p : parts) {
			if (p.isEmpty())
				return false;
		}
		for (int i = 0; i < parts.length - 1; i++) {
			if (!isValidName(parts[i]))
				return false;
		}
		return true;
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy