En esta guía vamos a explicar cómo añadir una nueva entidad de la Config DB al sistema de versionado de plataforma.
La funcionalidad que sustenta el sistema de versionado de recursos es bastante genérica, y está basada en el uso de la interfaz Versionable<T> , por lo que el esfuerzo que se requiere para añadir una entidad a este sistema es mínimo. A continuación detallaremos los pasos a seguir.
1. Implementación de la interfaz Versionable<T>
public interface Versionable<T> { public default String serialize() throws IOException { return new YAMLMapper().writeValueAsString(this); } @SuppressWarnings("unchecked") public default T deserialize(String content) throws IOException { return (T) new YAMLMapper().readValue(content, getClass()); } public String fileName(); public Object getId(); }
La interfaz Versionable sirve para indicar que una clase, en este caso una entidad JPA, debe pertenecer al sistema de versionado de la plataforma. En dicha interfaz se indican 4 métodos genéricos que serán necesario para las diferentes fases del versionado: serialización, deserialización, restauración…
Los métodos serialize y deserialize se implementan por defecto, ya que para la mayoría de entidades (entidades sin relaciones JPA o sin código HTML) no hará falta sobreescribirlos. Un caso ejemplo de entidad que necesitaría sobreescribir estos métodos sería Dashboard o Ontology.
En el caso de Ontology se necesita sobreescribir para evitar que en la fase de serialización a Yaml cargue por completo las relaciones. Las relaciones siempre las representaremos por el ID único de base de datos, por ejemplo (Ontology.java):
@Override public String serialize() { final YAMLMapper mapper = new YAMLMapper(); final ObjectNode node = new YAMLMapper().valueToTree(this); node.put("dataModel", dataModel == null ? null : dataModel.getId()); node.put("ontologyKPI", ontologyKPI == null ? null : ontologyKPI.getId()); node.put("ontologyTimeSeries", ontologyTimeSeries == null ? null : ontologyTimeSeries.getId()); final ArrayNode n = mapper.createArrayNode(); ontologyUserAccesses.forEach(oua -> n.add(oua.getId())); node.set("ontologyUserAccesses", n); try { return mapper.writeValueAsString(node); } catch (final JsonProcessingException e) { return null; } }
Si os fijáis las relaciones con DataModel, OntologyKPI, etc. , que a su vez son entidades, se sustituyen por el ID de la entidad en cuestión, si es que existe.
En el caso de Dashboard, lo sobreescribimos para representar el HTML dentro del Yaml correctamente (escapándolo con un wrapper, clase HTML.java)
@Override public String serialize() throws IOException { final ObjectMapper mapper = new ObjectMapper(); final Map<String, Object> map = mapper.convertValue(this, new TypeReference<Map<String, Object>>() {}); map.put("headerlibs", new HTML(headerlibs)); prettyHTML5Gadget((Map<String, Object>) map.get("model")); return VersioningUtils.versioningYaml(this.getClass()).dump(map); }
La clase HTML tiene asociada un Tag yaml -!html- que se usa para representar e interpretar el contenido en la serialización y deserialización del fichero, se puede ver en la última línea del método: VersioningUtils.versioningYaml(this.getClass()).dump(map);
public class VersioningUtils { public static <T extends Object> Yaml versioningYaml(Class<T> clazz) { final DumperOptions opts = new DumperOptions(); opts.setDefaultFlowStyle(FlowStyle.BLOCK); opts.setIndent(4); opts.setPrettyFlow(true); opts.setSplitLines(true); opts.setAllowUnicode(true); final HTMLRepresenter rep = new HTMLRepresenter(); rep.addClassTag(clazz, Tag.MAP); return new Yaml(new HTMLConstructor(), rep, opts); } }
public class HTMLConstructor extends SafeConstructor { public HTMLConstructor() { yamlConstructors.put(new Tag("!html"), new YamlHTMLConstructor()); } private class YamlHTMLConstructor extends AbstractConstruct { @Override public Object construct(Node node) { final String val = (String) constructScalar((ScalarNode) node); return new HTML(val); } } }
public class HTMLRepresenter extends Representer { public HTMLRepresenter() { representers.put(HTML.class, new YamlHTMLRepresenter()); representers.put(String.class, new YamlStringRepresenter()); } private class YamlHTMLRepresenter implements Represent { @Override public Node representData(Object data) { final HTML html = (HTML) data; String value = html.getHTMLContent(); //replace tabs for 3 spaces & invalid linebreaks with spaces value = value.replace("\t", " ").replaceAll("([ ]+\\n)", "\n"); defaultScalarStyle = ScalarStyle.LITERAL.getChar(); return representScalar(new Tag("!html"), value); } } private class YamlStringRepresenter implements Represent { @Override public Node representData(Object data) { final String value = (String) data; defaultScalarStyle = ScalarStyle.PLAIN.getChar(); return representScalar(Tag.STR, value); } } }
public class HTML { private String HTMLContent; }
Dicho esto, el primer paso es implementar esta interfaz, tomaremos como ejemplo la entidad API.
public class Api extends OPResource implements Versionable<Api>
A continuación tendremos que implementar los dos métodos de la interfaz que no tienen un default, getId() y fileName(), el primero no hará falta ya que al ser entidades del sistema, todos tienen un atributo id (salvo User por ejemplo). Con lo cuál automáticamente se detecta por la interfaz.
En el método fileName() lo que se devuelve es el nombre del fichero donde quedará la entidad serializada, la convención que estamos usando es identification + _ + id + .yml, para las entidades que tengan ambos atributos, si no únicamente se usa el id + .yml
@Override public String fileName() { return getIdentification() + "_" + getId() + ".yaml"; }
A continuación identificamos los atributos de la entidad que son relaciones JPA, en el caso de Api serían: ontology y userApiAccesses. Estos serán los atributos que nos obligarán a sobreescribir el método serialize, y como dijimos anteriormente, tenemos que sustituirlos por el Id en caso de existir, por lo que quedaría así:
@Override public String serialize() throws IOException { final YAMLMapper mapper = new YAMLMapper(); final ObjectNode node = new YAMLMapper().valueToTree(this); node.put("ontology", ontology == null ? null : ontology.getId()); final ArrayNode n = mapper.createArrayNode(); userApiAccesses.forEach(uaa -> n.add(uaa.getId())); node.set("userApiAccesses", n); try { return mapper.writeValueAsString(node); } catch (final JsonProcessingException e) { return null; } }
Ahora para el proceso de deserialización, tendremos que tener en cuenta que hemos sustituido entidades por un String id, por lo que tendremos que indicar como deserializar esos campos: ontology y userApiAccesses.
Para esto, la mejor opción es crear un @JsonSetter ya que estamos utilizando la librería de Jackson internamente. Por ejemplo para estos dos atributos crearíamos los siguientes:
@JsonSetter("ontology") public void setOntologyJson(String id) { if (!StringUtils.isEmpty(id)) { final Ontology o = new Ontology(); o.setId(id); ontology = o; } } @JsonSetter("ontologyUserAccesses") public void setUserApiAccessesJson(Set<String> userApiAccesses) { userApiAccesses.forEach(s -> { final UserApi uaa = new UserApi(); uaa.setId(s); this.userApiAccesses.add(uaa); }); }
Con establecer el Id en la entidad correspondiente (p.e. Ontology) es suficiente, no hace falta traerse la entidad entera de base de datos, es una de las ventajas de JPA.
2. Añadir repositorio a VersioningRepositoryFacade
Existe un repositorio Facade que gestiona los diferentes repositorios de las entidades versionable, para poder ser invocado de manera sencilla por las capas de servicio superiores.
Tendríamos que añadir a la clase VersioningRepositoryFacade lo siguiente para el caso de Api.
@Autowired private ApiRepository apiRepository; private static final String API="Api"; //Nombre de la clase Java, respetar Mayus. .... @SuppressWarnings("unchecked") public <R extends JpaRepository<T, I>, T, I> R getJpaRepository(T versionable) { switch (versionable.getClass().getSimpleName()) { .... case API: return (R) apiRepository; .... } @SuppressWarnings("unchecked") public <S> S save(Versionable<S> versionable) { switch (versionable.getClass().getSimpleName()) { ..... case API: return (S) apiRepository.save((Api) versionable); ..... }
3. Consideraciones extra si la entidad no extiende de OPResource
Para el caso de entidades versionables que no extiendan de OPResource habrá dos casos:
La entidad tiene atributo user
En este caso tendremos que ir al repositorio de la entidad, tomemos como ejemplo DashboardUserAccess y DashboardUserAccessRepository, y asegurarnos de que existe el método findByUser, si no, lo creamos:
public interface DashboardUserAccessRepository extends JpaRepository<DashboardUserAccess, String> { ... List<DashboardUserAccess> findByUser(User user); }
La entidad no tiene atributo user
En este caso iremos al repositorio de la entidad, si podemos hacer una query que relacione un usuario con la entidad la crearemos, y si no, crearemos el método findByUser como un default tal como en los siguientes ejemplos:
@Query("SELECT t FROM Token t WHERE t.clientPlatform.user= :#{#user}") List<Token> findByUser(User user);
public interface DashboardConfRepository extends JpaRepository<DashboardConf, String> { .... default List<DashboardConf> findByUser(User user){ return findAll(); } ... }
Esto es necesario para que funcionen ciertas funcionalidades para usuarios no administradores.