package org.folio.okapi.bean; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.folio.okapi.util.ProxyContext; /** * InterfaceDescriptor describes an interface a module can provide, or depend * on. * Basically just a name, and a version number. Version numbers are in the form * X.Y.Z where X is the major version of the interface, Y is the minor version * of the interface, and Z is the software version of the module. Also * the InterfaceType, and the routing entries for the interface. */ // S1168: Empty arrays and collections should be returned instead of null // S1192: String literals should not be duplicated @java.lang.SuppressWarnings({"squid:S1168", "squid:S1192"}) @JsonInclude(JsonInclude.Include.NON_NULL) public class InterfaceDescriptor { private String id; private String version; private String interfaceType; // enum: "proxy" (default), "system", "internal", multiple private RoutingEntry[] handlers; private String[] scope; /** * Create interface descriptor. */ public InterfaceDescriptor() { } /** * Create interface descriptor with ID and version. * @param id interface ID * @param version interface version */ public InterfaceDescriptor(String id, String version) { this.id = id; if (validateVersion(version)) { this.version = version; } else { throw new IllegalArgumentException("Bad version number '" + version + "'"); } } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getVersion() { return version; } /** * set interface version. * @param version interface version */ public void setVersion(String version) { if (validateVersion(version)) { this.version = version; } else { throw new IllegalArgumentException("Bad version number '" + version + "'"); } } /** * Validate that the version conforms to the format XX.YY.ZZ or XX.YY * * @return true if a good version number */ public static boolean validateVersion(String version) { int[] p = versionParts(version, 0); return p != null; } /** * Return the version parts. * * @param version full interface version * @return an array of 3 elements, XX, YY, ZZ, with -1 for missing parts */ private static int[] versionParts(String version, int idx) { final String[] verComp = version.split(" "); if (verComp.length <= idx) { return null; } final String[] parts = verComp[idx].split("\\."); if (parts.length < 2 || parts.length > 3) { return null; } int[] p = new int[3]; for (int i = 0; i < 3; i++) { if (i < parts.length) { try { p[i] = Integer.parseInt(parts[i]); } catch (NumberFormatException ex) { return null; } } else { p[i] = -1; } } return p; } /** * Check if this InterfaceDescriptor is compatible with the required one. * * @param required interface that is required */ public boolean isCompatible(InterfaceDescriptor required) { if (!this.getId().equals(required.getId())) { return false; // not the same interface at all } int[] t = InterfaceDescriptor.versionParts(this.version, 0); if (t == null) { return false; } for (int idx = 0;; idx++) { int[] r = InterfaceDescriptor.versionParts(required.version, idx); if (r == null) { break; } if (t[0] == r[0] && (t[1] > r[1] || (t[1] == r[1] && r[2] <= t[2]))) { return true; } } return false; } public String getInterfaceType() { return interfaceType; } public void setInterfaceType(String interfaceType) { this.interfaceType = interfaceType; } /** * Checks if the interface is a regular handler. Not a system interface, not * multiple, and not old-fashioned _tenant. Used to skip conflict checks. */ @JsonIgnore public boolean isRegularHandler() { return isType("proxy"); } /** * checks if interface descriptor is certain type. * @param type type to check */ @JsonIgnore public boolean isType(String type) { String haveType; if (id.startsWith("_")) { haveType = "system"; } else if (interfaceType == null) { haveType = "proxy"; } else { haveType = interfaceType; } return type.equals(haveType); } /** * Get routing entries as list. */ @JsonIgnore public List<RoutingEntry> getAllRoutingEntries() { List<RoutingEntry> all = new ArrayList<>(); if (handlers != null) { Collections.addAll(all, handlers); } return all; } public RoutingEntry[] getHandlers() { return handlers; } public void setHandlers(RoutingEntry[] handlers) { this.handlers = handlers; } /** * Validate a moduleInterface. * Writes Warnings in the log in case of deprecated features. * * @param section "provides" or "requires" - the rules differ * @return "" if ok, or a simple error message */ public String validate(ProxyContext pc, String section, String mod) { if (id == null) { return "id is missing for module " + mod; } String prefix = "Module '" + mod + "' interface '" + id + "': "; if (version == null) { return "version is missing for module " + mod; } String err; err = validateGeneral(mod); if (!err.isEmpty()) { return err; } if (version.matches("\\d+\\.\\d+\\.\\d+")) { pc.warn(prefix + "has a 3-part version number '" + version + "'." + "Interfaces should be 2-part"); } if ("_tenant".equals(this.id) && !"1.0 1.1 1.2".contains(version)) { pc.warn(prefix + " is '" + version + "'." + " should be '1.0/1.1/1.2'"); } if ("_tenantPermissions".equals(this.id) && !"1.0 1.1".contains(version)) { pc.warn(prefix + " is '" + version + "'. should be '1.0/1.1'"); } if (section.equals("provides")) { err = validateProvides(pc, mod); if (!err.isEmpty()) { return err; } } if (section.equals("requires")) { err = validateRequires(mod); if (!err.isEmpty()) { return err; } } return ""; } /** * Validate those things that just have to be right. * @return "" if ok, or an error message */ private String validateGeneral(String mod) { String it = getInterfaceType(); if (it != null && !it.equals("proxy") && !it.equals("system") && !it.equals("multiple")) { return "Bad interface type '" + it + "' for module " + mod; } if (!validateVersion(version)) { return "Bad interface version number '" + version + "' for module " + mod; } return ""; } /** * Validate those things that apply to the "provides" section. */ private String validateProvides(ProxyContext pc, String mod) { if (handlers != null && handlers.length > 0) { boolean checkPermissionsRequired = !isType("system"); for (RoutingEntry re : handlers) { String err = re.validateHandlers(pc, mod); if (err.isEmpty() && checkPermissionsRequired) { err = validatePermissionsRequired(pc, mod, re); } if (!err.isEmpty()) { return err; } } } return ""; } /* * Validate the required field "permissionsRequired" */ private String validatePermissionsRequired(ProxyContext pc, String mod, RoutingEntry re) { if (re.getPermissionsRequired() != null) { return ""; } String path = re.getPathPattern() == null ? "" : re.getPathPattern(); path += re.getPath() == null ? "" : re.getPath(); path = path.trim().isEmpty() ? "" : " " + path.trim(); String err = "Module '" + mod + "' " + "handler" + path + ": Missing field permissionsRequired"; pc.warn(err); return err; } /** * Validate those things that apply to the "requires" section. */ private String validateRequires(String mod) { if (handlers != null && handlers.length > 0) { return "No handlers allowed in 'requires' section for module " + mod; } return ""; } /** * Get scopes for interface (possibly empty). */ @JsonIgnore public List<String> getScopeArray() { if (scope == null) { return new ArrayList<>(); } else { return Arrays.asList(scope); } } public String[] getScope() { return scope; } public void setScope(String[] scope) { this.scope = scope; } }