com.xlrit.gears.server.graphql.ViewResolver Maven / Gradle / Ivy
The newest version!
package com.xlrit.gears.server.graphql;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.xlrit.gears.base.exception.AuthException;
import com.xlrit.gears.base.util.Range;
import com.xlrit.gears.engine.meta.*;
import com.xlrit.gears.engine.security.AuthManager;
import com.xlrit.gears.engine.snel.EvaluatorContext;
import com.xlrit.gears.engine.snel.EvaluatorScope;
import jakarta.persistence.EntityManager;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.hibernate.envers.AuditReaderFactory;
import org.hibernate.envers.DefaultRevisionEntity;
import org.hibernate.envers.query.AuditEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
@Controller("graphqlViewResolver")
@RequiredArgsConstructor
public class ViewResolver {
private static final Logger LOG = LoggerFactory.getLogger(ViewResolver.class);
private final ViewManager viewManager;
private final MetaManager metaManager;
private final AuthManager authManager;
private final EntityManager entityManager;
// === top level === //
@QueryMapping
@PreAuthorize("isAuthenticated()")
public MainListView list(@Argument String key) {
LOG.debug("list(key={})", key);
ViewManager.ViewDefinitions.ListDef listDef = viewManager.getListDef(key);
if (listDef.hasRequiredRoles() && !authManager.hasAnyRole(listDef.getRoles())) {
throw new AuthException("Not authorized to view list '" + key + "'");
}
return new MainListView(listDef);
}
@QueryMapping
@PreAuthorize("isAuthenticated()")
public DetailView detail(@Argument String key) {
LOG.debug("detail(key={})", key);
ViewManager.ViewDefinitions.DetailDef detailDef = viewManager.getDetailDef(key);
if (detailDef.hasRequiredRoles() && !authManager.hasAnyRole(detailDef.getRoles())) {
throw new AuthException("Not authorized to view detail '" + key + "'");
}
EntityInfo> entityInfo = metaManager.requireEntityInfo(detailDef.getEntity());
return new DetailView(detailDef, entityInfo);
}
// === main list view === //
@SchemaMapping
public Link link(MainListView listView, @Argument String key) {
return listView.link(key);
}
@SchemaMapping
public long count(MainListView listView, @Argument String filter) {
return listView.count(filter);
}
@SchemaMapping
public JsonNode data(MainListView listView, @Argument String filter, @Argument Range range) {
LOG.debug("data(listView={})", listView.getId());
return listView.data(filter, range);
}
// === nested list view === //
@SchemaMapping
public long count(DetailView.DetailInstance.NestedListView listView, @Argument String filter) {
long result = listView.count(filter);
LOG.debug("count for {}: result={}", listView, result);
return result;
}
@SchemaMapping
public JsonNode data(DetailView.DetailInstance.NestedListView listView, @Argument String filter, @Argument Range range) {
LOG.debug("data for {}", listView);
return listView.data(filter, range);
}
// === detail view === //
@SchemaMapping
public DetailView.DetailInstance instance(DetailView detailView, @Argument String id) {
LOG.debug("instance(detailView={})", detailView.getId());
return detailView.instance(id);
}
// === detail instance === //
@SchemaMapping
public DetailView.DetailInstance.NestedListView list(DetailView.DetailInstance instance, @Argument String key) {
return instance.list(key);
}
@SchemaMapping
public Link link(DetailView.DetailInstance instance, @Argument String key) {
return instance.link(key);
}
// === helper === //
private EvaluatorContext createEvaluatorContext(PrintOptions printOptions, EvaluatorScope scope) {
Printer printer = metaManager.printer(printOptions);
return new EvaluatorContext(printer, scope);
}
// === inner types === //
public interface ListView {
long count(String filter);
JsonNode data(String filter, Range range);
}
@Data
public class MainListView implements ListView {
private final ViewManager.ViewDefinitions.ListDef listDef;
public String getId() {
return listDef.getKey();
}
public JsonNode getSchema() {
return viewManager.listSchema(listDef);
}
public List getLinks() {
return listDef.getLinks().stream()
.map(linkDef -> new GenericLink(listDef.getFqk(), linkDef))
.collect(Collectors.toList());
}
public Link link(String key) {
return listDef.getLinks().stream()
.filter(linkDef -> key.equals(linkDef.getKey()))
.map(linkDef -> new GenericLink(listDef.getFqk(), linkDef))
.findAny().orElseThrow();
}
public long count(String filter) {
return viewManager.listCount(listDef, filter);
}
public JsonNode data(String filter, Range range) {
return viewManager.listData(listDef, filter, range);
}
}
@Data
public class DetailView {
private final ViewManager.ViewDefinitions.DetailDef detailDef;
private final EntityInfo> entityInfo;
public String getId() {
return detailDef.getKey();
}
public JsonNode getSchema() {
return viewManager.detailSchema(detailDef);
}
public List getLinks() {
return detailDef.getLinks().stream()
.map(linkDef -> new GenericLink(detailDef.getFqk(), linkDef))
.collect(Collectors.toList());
}
public DetailInstance instance(String id) {
return new DetailInstance(id);
}
public boolean isAudited() {
Class> clz = entityInfo.getObjectClass();
return AuditReaderFactory.get(entityManager).isEntityClassAudited(clz);
}
@Data
public class DetailInstance {
private final String id;
private JsonNode mainData; // initialized lazily
private EvaluatorContext evaluatorCtx; // initialized lazily
protected EvaluatorContext getEvaluatorCtx() {
if (this.evaluatorCtx == null) {
Printer printer = metaManager.printer(PrintOptions.DEFAULT);
EvaluatorScope scope = viewManager.createEvaluatorScope(detailDef, id);
this.evaluatorCtx = new EvaluatorContext(printer, scope);
}
return this.evaluatorCtx;
}
public JsonNode getData() {
if (this.mainData == null) {
LOG.debug("Loading data for detail view key={}, id={}", detailDef.getKey(), id);
this.mainData = viewManager.detailData(detailDef, id);
}
return this.mainData;
}
public List getEnabledSectionKeys() {
EvaluatorContext evaluatorCtx = getEvaluatorCtx();
return detailDef.getSections().stream()
.filter(section -> section.isEnabled(evaluatorCtx))
.map(ViewManager.ViewDefinitions.DetailDef.SectionDef::getKey)
.toList();
}
public NestedListView list(String sectionKey) {
ViewManager.ViewDefinitions.DetailDef.SectionDef section = detailDef.getSection(sectionKey);
return new NestedListView(section);
}
public List getLinks() {
return detailDef.getLinks().stream()
.map(linkDef -> new DetailLink(detailDef.getFqk(), linkDef))
.collect(Collectors.toList());
}
public Link link(String key) {
return detailDef.getLinks().stream()
.filter(linkDef -> key.equals(linkDef.getKey()))
.map(linkDef -> new DetailLink(detailDef.getFqk(), linkDef))
.findAny().orElseThrow();
}
@SuppressWarnings("unchecked")
public List getRevisions() {
List revisions =
AuditReaderFactory.get(entityManager).createQuery()
.forRevisionsOfEntity(entityInfo.getObjectClass(), false)
.add(AuditEntity.id().eq(id))
.getResultList();
LOG.info("Returning {} revisions for {} #{}", revisions.size(), entityInfo.getTypeName(), id);
return revisions.stream().map(Revision::fromRevisionEntity).toList();
}
@Data
public class NestedListView implements ListView {
private final ViewManager.ViewDefinitions.DetailDef.SectionDef sectionDef;
private final boolean disabled;
public NestedListView(ViewManager.ViewDefinitions.DetailDef.SectionDef sectionDef) {
this.sectionDef = sectionDef;
this.disabled = sectionDef.isConditional() && sectionDef.isEnabled(getEvaluatorCtx()) != Boolean.TRUE;
}
@Override
public long count(String filter) {
if (filter != null && !filter.isBlank()) throw new RuntimeException("Filtering is currently not supported for nested lists");
if (disabled) return -1;
return viewManager.nestedListCount(detailDef, sectionDef, id);
}
@Override
public JsonNode data(String filter, Range range) {
if (disabled) return NullNode.getInstance();
LOG.debug("Loading data for nested list view key={}, section={}, id={}", detailDef.getKey(), sectionDef.getKey(), id);
return viewManager.nestedListData(detailDef, sectionDef, id, range);
}
@Override
public String toString() {
return "NestedListView[detail=" + detailDef.getKey() + ", sectionDef=" + sectionDef.getKey() + "]";
}
}
public class DetailLink extends Link {
public DetailLink(String parentFqk, ViewManager.ViewDefinitions.LinkDef linkDef) {
super(parentFqk, linkDef);
}
@Override
public Boolean getEnabled() {
return linkDef.isEnabled(getEvaluatorCtx());
}
@Override
public JsonNode getFormValues() {
return viewManager.getFormValues(linkDef, getEvaluatorCtx());
}
}
}
}
@RequiredArgsConstructor
public abstract static class Link {
protected final String parentFqk;
protected final ViewManager.ViewDefinitions.LinkDef linkDef;
public String getId() { return parentFqk + ":" + linkDef.getKey(); }
public String getKey() { return linkDef.getKey(); }
public String getRef() { return linkDef.getRef(); }
public String getLabel() { return linkDef.getLabel(); }
public String getIcon() { return linkDef.getIcon(); }
public boolean isConditional() { return linkDef.isConditional(); }
public String getProcessKey() { return linkDef.getProcessKey(); }
public ViewManager.LinkKind getKind() { return linkDef.getKind(); }
public abstract Boolean getEnabled();
public abstract JsonNode getFormValues();
}
public class GenericLink extends Link {
public GenericLink(String parentFqk, ViewManager.ViewDefinitions.LinkDef linkDef) {
super(parentFqk, linkDef);
}
public Boolean getEnabled() {
return isConditional() ? null : true; // null is unknown
}
@Override
public JsonNode getFormValues() {
EvaluatorContext evaluatorCtx = createEvaluatorContext(PrintOptions.DEFAULT, EvaluatorScope.ROOT);
return viewManager.getFormValues(linkDef, evaluatorCtx);
}
}
public record Revision(int rev, OffsetDateTime timestamp) {
static Revision fromRevisionEntity(DefaultRevisionEntity r) {
OffsetDateTime timestamp = OffsetDateTime.ofInstant(r.getRevisionDate().toInstant(), ZoneId.systemDefault());
return new Revision(r.getId(), timestamp);
}
}
}