blob: 8bc8e42e9c36abddebd7dde6be0570b6e181dcd8 [file] [log] [blame]
/*
* Copyright 2023 Couchbase, Inc.
*
* 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
*
* http://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 com.couchbase.atlas.connector.entities;
import org.apache.atlas.AtlasClientV2;
import org.apache.atlas.AtlasServiceException;
import org.apache.atlas.model.instance.AtlasEntity;
import org.apache.atlas.model.typedef.AtlasStructDef;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
/**
* Base class for all couchbase atlas models
* The class uses "Self-Builder" pattern:
* 1. First, create the "builder" instance of the class
* 2. Populate the identifying fields of the class (check the `qualifiedName` method of the entity for the list)
* (all setters return the instance just as a Builder would)
* 3. Call `get()` method to resolve the instance and replace it with previously loaded from Atlas data (if present)
*
* Example:
* ```java
* clusterEntity = new CouchbaseCluster()
* .name(CBConfig.address())
* .url(CBConfig.address())
* .get();
* ```
*
* @param <E> extending class
*/
public abstract class CouchbaseAtlasEntity<E extends CouchbaseAtlasEntity<?>> {
private static final Map<Class, Map<String, AtlasEntity>> ENTITY_BY_TYPE_AND_ID = Collections.synchronizedMap(new HashMap<>());
private static final Map<Class, Map<String, CouchbaseAtlasEntity>> MODEL_BY_TYPE_AND_ID = Collections.synchronizedMap(new HashMap<>());
private String name;
public String name() {
return name;
}
public E name(String name) {
this.name = name;
return (E) this;
}
/**
* Loads or creates corresponding Atlas Entity and marks this model as existing in Atlas
*
* @param atlas
* @return
*/
public AtlasEntity atlasEntity(AtlasClientV2 atlas) {
AtlasEntity atlasEntity = atlasEntity()
.filter(entity -> entity.getGuid().charAt(0) != '-')
.orElseGet(() ->
cache(load(atlas)
.orElseGet(() ->
atlasEntity()
.orElseGet(() -> new AtlasEntity(atlasTypeName())))
)
);
atlasEntity.setAttribute("name", name);
atlasEntity.setAttribute("qualifiedName", qualifiedName());
updateAtlasEntity(atlasEntity);
return atlasEntity;
}
protected abstract String qualifiedName();
/**
* Looks up precreated atlas entity in the entity cache
* @return Optional of the cached entity
*/
public Optional<AtlasEntity> atlasEntity() {
return cachedEntity().map(atlasEntity -> {
updateAtlasEntity(atlasEntity);
return atlasEntity;
});
}
/**
* Checks whether the model has the Atlas Entity created for it
* by looking it up in the entity cache.
* NOTE: this method does not check if the entity has been saved in Atlas so,
* it will return true when the entity is already created and cached but is yet to be sent to Atlas
*
* This method is _mostly_ used in related objects when setting relationship field to ensure that related
* model has an AtlasEntity that can be referenced when storing relationship information.
*
* @return true if the entity found
*/
protected boolean exists() {
return cachedEntity().isPresent();
}
public abstract String atlasTypeName();
public abstract UUID id();
/**
* Invoked when the entity needs to be updated with values from the model
* @param entity the entity to write the values into
*/
protected void updateAtlasEntity(AtlasEntity entity) {
// override me
}
/**
* Invoked when the model needs to be updated with values from the entity
* @param entity the entity to read the values from
*/
protected void updateJavaModel(AtlasEntity entity) {
// override me
}
/**
* Loads the entity for this model from Atlas and stores it in the entity cache
* @param client Atlas client to use
* @return loaded entity
*/
private Optional<AtlasEntity> load(AtlasClientV2 client) {
try {
Map<String, String> query = new HashMap<>();
query.put("qualifiedName", qualifiedName());
AtlasEntity atlasEntity = client.getEntityByAttribute(atlasTypeName(), query).getEntity();
if (atlasEntity != null) {
cache(atlasEntity);
if (atlasEntity.hasAttribute("name")) {
this.name = (String) atlasEntity.getAttribute("name");
}
updateJavaModel(atlasEntity);
return Optional.of(atlasEntity);
}
} catch (AtlasServiceException e) {
if (e.getStatus().getStatusCode() != 404) {
throw new RuntimeException(e);
}
}
return Optional.empty();
}
/**
* Puts an entity into the entity cache
* @param atlasEntity the entity to cache
* @return the same entity
*/
private AtlasEntity cache(AtlasEntity atlasEntity) {
if (!ENTITY_BY_TYPE_AND_ID.containsKey(getClass())) {
ENTITY_BY_TYPE_AND_ID.put(getClass(), new HashMap<>());
}
ENTITY_BY_TYPE_AND_ID.get(getClass()).put(id().toString(), atlasEntity);
return atlasEntity;
}
/**
* Looks up the entity in the cache
* @return Optional of cached entity
*/
private Optional<AtlasEntity> cachedEntity() {
return Optional.ofNullable(ENTITY_BY_TYPE_AND_ID.getOrDefault(getClass(), (Map<String, AtlasEntity>) Collections.EMPTY_MAP).getOrDefault(id().toString(), null));
}
/**
* First checks if the entity has been loaded and cached and, if not, then tries to load it from Atlas
* @param atlas Atlas client to use
* @return true if the entity found either in cache or in Atlas
*/
public boolean exists(AtlasClientV2 atlas) {
if (!exists()) {
return load(atlas).isPresent();
}
return true;
}
/**
* Returns pre-cached model with provided identifiers or caches this model and returns it
*
* @return the model
*/
public E get() {
Class<E> type = (Class<E>) getClass();
String id = id().toString();
// ensure valid cache structure
if (!MODEL_BY_TYPE_AND_ID.containsKey(type)) {
MODEL_BY_TYPE_AND_ID.put(type, Collections.synchronizedMap(new HashMap<>()));
}
// put the model into the cache, if not already present
Map<String, CouchbaseAtlasEntity> modelsById = MODEL_BY_TYPE_AND_ID.get(type);
if (!modelsById.containsKey(id)) {
try {
modelsById.put(id, this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return (E) modelsById.get(id);
}
public static void dropCache() {
ENTITY_BY_TYPE_AND_ID.clear();
MODEL_BY_TYPE_AND_ID.clear();
}
}