Add store dependency model

This commit is contained in:
crschnick
2026-03-10 16:08:20 +00:00
parent c491dcd3c9
commit aed10f7508
26 changed files with 239 additions and 22 deletions
@@ -1,10 +1,15 @@
package io.xpipe.app.ext;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.xpipe.app.storage.DataStoreEntryRef;
import java.util.List;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public interface DataStore {
List<DataStoreEntryRef<?>> getDependencies();
default boolean isComplete() {
try {
checkComplete();
@@ -0,0 +1,30 @@
package io.xpipe.app.ext;
import io.xpipe.app.storage.DataStoreEntryRef;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class DataStoreDependencies {
public static List<DataStoreEntryRef<?>> of(Object... dependencies) {
var l = new ArrayList<DataStoreEntryRef<?>>();
for (Object dependency : dependencies) {
if (dependency instanceof DataStoreEntryRef<?> r) {
l.add(r);
} else if (dependency instanceof List<?> li) {
l.addAll(of(li));
}
}
return l;
}
public static List<DataStoreEntryRef<?>> of(DataStoreEntryRef<?>... dependencies) {
return Arrays.stream(dependencies).toList();
}
public static <T extends DataStore> List<DataStoreEntryRef<?>> of(List<DataStoreEntryRef<T>> refs) {
return new ArrayList<>(refs);
}
}
@@ -0,0 +1,10 @@
package io.xpipe.app.ext;
import io.xpipe.app.storage.DataStoreEntryRef;
import java.util.List;
public interface DependentDataStore extends DataStore {
List<DataStoreEntryRef<?>> getDependencies();
}
@@ -2,10 +2,17 @@ package io.xpipe.app.ext;
import io.xpipe.app.storage.DataStoreEntryRef;
import java.util.List;
public interface GroupStore<T extends DataStore> extends DataStore {
DataStoreEntryRef<? extends T> getParent();
@Override
default List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(getParent());
}
@Override
default void checkComplete() throws Throwable {
var p = getParent();
@@ -7,6 +7,8 @@ import io.xpipe.app.storage.DataStoreEntryRef;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.Value;
import java.util.List;
@JsonTypeName("local")
@Value
public class LocalStore implements NetworkTunnelStore, ShellStore, StatefulDataStore<ShellStoreState> {
@@ -39,4 +41,9 @@ public class LocalStore implements NetworkTunnelStore, ShellStore, StatefulDataS
public NetworkTunnelSession createTunnelSession(int localPort, int remotePort, String address) {
return null;
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return List.of();
}
}
@@ -55,6 +55,7 @@ public abstract class DataStorage {
private final Map<DataStore, DataStoreEntry> identityStoreEntryMapCache = new IdentityHashMap<>();
private final Map<DataStore, DataStoreEntry> storeEntryMapCache = new HashMap<>();
private final Map<DataStore, DataStore> storeMoveCache = new IdentityHashMap<>();
private final Map<DataStoreEntry, Collection<DataStoreEntryRef<?>>> storeDependencyCache = new HashMap<>();
@Getter
protected boolean entriesAvailable;
@@ -362,7 +363,7 @@ public abstract class DataStorage {
var newParent = DataStorage.get().getDefaultDisplayParent(newEntry);
var sameParent = Objects.equals(oldParent, newParent);
finalizeWithChildren(entry);
finalizeWithDependencies(entry);
var children = getDeepStoreChildren(entry);
if (!sameParent) {
@@ -377,6 +378,9 @@ public abstract class DataStorage {
synchronized (storeEntryMapCache) {
storeEntryMapCache.remove(entry.getStore());
}
synchronized (storeDependencyCache) {
storeDependencyCache.remove(entry);
}
}
var categoryChanged = !entry.getCategoryUuid().equals(newEntry.getCategoryUuid());
@@ -412,25 +416,42 @@ public abstract class DataStorage {
saveAsync();
}
private void finalizeWithChildren(DataStoreEntry entry) {
var c = getDeepStoreChildren(entry);
var l = new ArrayList<>(c);
l.addFirst(entry);
for (int i = l.size() - 1; i >= 0; i--) {
l.get(i).finalizeEntry();
public void finalizeWithDependencies(DataStoreEntry entry) {
var cached = storeDependencyCache.computeIfAbsent(entry, this::getDependencies);
for (DataStoreEntryRef<?> dataStoreEntryRef : cached) {
dataStoreEntryRef.get().finalizeEntry();
}
}
private Collection<DataStoreEntryRef<?>> getDependencies(DataStoreEntry entry) {
var l = new HashSet<DataStoreEntryRef<?>>();
var store = entry.getStore();
if (store == null) {
return l;
}
var deps = store.getDependencies();
l.addAll(deps);
for (DataStoreEntryRef<?> dep : deps) {
l.addAll(getDependencies(dep.get()));
}
return l;
}
public void updateEntryStore(DataStoreEntry entry, DataStore store) {
if (entry.getStore() == store) {
return;
}
finalizeWithChildren(entry);
finalizeWithDependencies(entry);
if (entry.getStore() != null && store != null && !entry.getStore().equals(store)) {
synchronized (storeMoveCache) {
storeMoveCache.put(entry.getStore(), store);
}
synchronized (storeDependencyCache) {
storeDependencyCache.put(entry, getDependencies(entry));
}
}
entry.setStoreInternal(store, false);
@@ -852,6 +873,9 @@ public abstract class DataStorage {
synchronized (storeEntryMapCache) {
storeEntryMapCache.remove(td.getStore());
}
synchronized (storeDependencyCache) {
storeDependencyCache.remove(td);
}
var parent = getDefaultDisplayParent(td);
parent.ifPresent(p -> p.setChildrenCache(null));
}
@@ -986,7 +1010,7 @@ public abstract class DataStorage {
}
public void deleteStoreEntry(@NonNull DataStoreEntry entry) {
finalizeWithChildren(entry);
finalizeWithDependencies(entry);
this.storeEntries.remove(entry);
synchronized (identityStoreEntryMapCache) {
identityStoreEntryMapCache.remove(entry.getStore());
@@ -1,6 +1,7 @@
package io.xpipe.ext.base.desktop;
import io.xpipe.app.ext.DataStore;
import io.xpipe.app.ext.DataStoreDependencies;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.Validators;
@@ -10,6 +11,9 @@ import lombok.Value;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.Collections;
import java.util.List;
@Value
@SuperBuilder
@Jacksonized
@@ -20,6 +24,11 @@ public class DesktopApplicationStore implements DataStore {
String path;
String arguments;
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(desktop);
}
@Override
public void checkComplete() throws Throwable {
Validators.nonNull(desktop);
@@ -10,6 +10,8 @@ import lombok.Value;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
@Value
@ToString(callSuper = true)
@SuperBuilder
@@ -20,6 +22,10 @@ public class AbstractHostStore implements DataStore, HostAddressStore, HostAddre
String host;
DataStoreEntryRef<NetworkTunnelStore> gateway;
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(gateway);
}
@Override
public void checkComplete() throws Throwable {
Validators.nonNull(host);
@@ -1,5 +1,6 @@
package io.xpipe.ext.base.identity;
import io.xpipe.app.ext.DataStoreDependencies;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.secret.EncryptedValue;
import io.xpipe.app.secret.SecretNoneStrategy;
@@ -19,6 +20,8 @@ import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = IdentityValue.InPlace.class),
@@ -93,6 +96,8 @@ public interface IdentityValue {
boolean isInPlace();
List<DataStoreEntryRef<?>> getDependencies();
default void checkCompleteUser() throws ValidationException {
Validators.nonNull(unwrap().getUsername().hasUser() ? new Object() : null, "Identity username");
}
@@ -136,6 +141,11 @@ public interface IdentityValue {
public boolean isInPlace() {
return true;
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return List.of();
}
}
@JsonTypeName("ref")
@@ -168,5 +178,10 @@ public interface IdentityValue {
public boolean isInPlace() {
return false;
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(ref);
}
}
}
@@ -6,12 +6,15 @@ import io.xpipe.app.secret.SecretRetrievalStrategy;
import io.xpipe.app.cred.SshIdentityStrategy;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.storage.DataStoreEntryRef;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.Value;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
@SuperBuilder(toBuilder = true)
@JsonTypeName("localIdentity")
@Jacksonized
@@ -45,4 +48,9 @@ public class LocalIdentityStore extends IdentityStore {
EncryptedValue<SshIdentityStrategy> getEncryptedSshIdentity() {
return sshIdentity;
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return List.of();
}
}
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.cred.SshIdentityStrategy;
import io.xpipe.app.cred.UsernameStrategy;
import io.xpipe.app.ext.DataStoreDependencies;
import io.xpipe.app.ext.InternalCacheDataStore;
import io.xpipe.app.ext.StatefulDataStore;
import io.xpipe.app.ext.ValidationException;
@@ -63,6 +64,7 @@ public class MultiIdentityStore extends IdentityStore implements StatefulDataSto
}
setState(MultiIdentityStoreState.builder().selected(entry.get().getUuid()).build());
DataStorage.get().finalizeWithDependencies(getSelfEntry());
}
private DataStoreEntryRef<IdentityStore> getSelectedOrThrow() {
@@ -73,6 +75,11 @@ public class MultiIdentityStore extends IdentityStore implements StatefulDataSto
throw ErrorEventFactory.expected(new IllegalStateException("No available identity for multi identity " + getSelfEntry().getName()));
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(getAvailableIdentities());
}
@Override
public void checkComplete() throws Throwable {
getSelectedOrThrow();
@@ -8,6 +8,7 @@ import io.xpipe.app.hub.action.StoreActionCategory;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.value.ObservableValue;
@@ -68,7 +69,9 @@ public class MultiIdentitySwitchBranchProvider implements HubBranchProvider<Mult
@Override
public void execute(DataStoreEntryRef<MultiIdentityStore> ref) {
ref.getStore().select(identity);
ThreadHelper.runAsync(() -> {
ref.getStore().select(identity);
});
}
@Override
@@ -11,6 +11,7 @@ import io.xpipe.app.process.ShellControl;
import io.xpipe.app.pwman.PasswordManager;
import io.xpipe.app.secret.*;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.*;
import com.fasterxml.jackson.annotation.JsonTypeName;
@@ -212,6 +213,11 @@ public class PasswordManagerIdentityStore extends IdentityStore
return new NoIdentityStrategy();
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return List.of();
}
@Override
public void checkComplete() throws Throwable {
Validators.nonNull(key);
@@ -9,12 +9,15 @@ import io.xpipe.app.cred.KeyFileStrategy;
import io.xpipe.app.cred.SshIdentityStrategy;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.storage.DataStoreEntryRef;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.Value;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
@SuperBuilder
@JsonTypeName("syncedIdentity")
@Value
@@ -44,6 +47,11 @@ public class SyncedIdentityStore extends IdentityStore implements UserScopeStore
return sshIdentity != null ? sshIdentity.getValue() : null;
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return List.of();
}
@Override
public void checkComplete() throws Throwable {
super.checkComplete();
@@ -1,6 +1,7 @@
package io.xpipe.ext.base.script;
import io.xpipe.app.ext.*;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.Validators;
import com.fasterxml.jackson.annotation.JsonTypeName;
@@ -20,6 +21,11 @@ public class ScriptCollectionSourceStore
ScriptCollectionSource source;
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return List.of();
}
@Override
public void checkComplete() throws Throwable {
Validators.nonNull(source);
@@ -114,6 +114,15 @@ public class ScriptStore implements SelfReferentialStore, StatefulDataStore<Enab
return ShellScript.of(content);
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
var l = DataStoreDependencies.of(scripts);
if (textSource != null) {
l.addAll(textSource.getDependencies());
}
return l;
}
@Override
public void checkComplete() throws Throwable {
if (textSource != null) {
@@ -6,6 +6,7 @@ import io.xpipe.app.comp.base.IntegratedTextAreaComp;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.ext.DataStoreDependencies;
import io.xpipe.app.ext.ShellDialectChoiceComp;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.hub.comp.StoreChoiceComp;
@@ -102,6 +103,11 @@ public interface ScriptTextSource {
property);
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return List.of();
}
@Override
public void checkComplete() {}
@@ -238,6 +244,11 @@ public interface ScriptTextSource {
return ShellScript.empty();
}
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return List.of();
}
}
@JsonTypeName("source")
@@ -359,6 +370,11 @@ public interface ScriptTextSource {
}
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(ref);
}
private ScriptCollectionSourceEntry findSourceEntryIfPossible() {
if (ref == null) {
return null;
@@ -393,6 +409,8 @@ public interface ScriptTextSource {
ShellScript getText();
List<DataStoreEntryRef<?>> getDependencies();
static List<Class<?>> getClasses() {
var l = new ArrayList<Class<?>>();
l.add(InPlace.class);
@@ -1,6 +1,7 @@
package io.xpipe.ext.base.service;
import io.xpipe.app.ext.DataStore;
import io.xpipe.app.ext.DataStoreDependencies;
import io.xpipe.app.ext.GroupStore;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.Validators;
@@ -12,6 +13,8 @@ import lombok.ToString;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder;
import java.util.List;
@Getter
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@SuperBuilder
@@ -1,5 +1,6 @@
package io.xpipe.ext.base.service;
import io.xpipe.app.ext.DataStoreDependencies;
import io.xpipe.app.ext.NetworkTunnelStore;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.ext.base.host.AbstractHostStore;
@@ -13,6 +14,8 @@ import lombok.ToString;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
@SuperBuilder(toBuilder = true)
@Getter
@Jacksonized
@@ -26,6 +29,11 @@ public final class CustomServiceStore extends AbstractServiceStore implements Ab
private final DataStoreEntryRef<NetworkTunnelStore> gateway;
private final Boolean tunnelToLocalhost;
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(host, gateway);
}
@Override
public void checkComplete() throws Throwable {
super.checkComplete();
@@ -1,6 +1,7 @@
package io.xpipe.ext.base.service;
import io.xpipe.app.ext.DataStore;
import io.xpipe.app.ext.DataStoreDependencies;
import io.xpipe.app.ext.FixedChildStore;
import io.xpipe.app.ext.NetworkTunnelStore;
import io.xpipe.app.storage.DataStoreEntryRef;
@@ -14,6 +15,7 @@ import lombok.ToString;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
import java.util.OptionalInt;
@SuperBuilder(toBuilder = true)
@@ -28,6 +30,11 @@ public class FixedServiceStore extends AbstractServiceStore implements FixedChil
private final DataStoreEntryRef<? extends DataStore> displayParent;
private final Boolean tunnelToLocalhost;
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(host);
}
@Override
public String getAddress() {
return null;
@@ -44,6 +44,11 @@ public class IncusContainerStore
String containerName;
IdentityValue identity;
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(install, identity != null ? identity.getDependencies() : null);
}
@Override
public Class<NetworkContainerStoreState> getStateClass() {
return NetworkContainerStoreState.class;
@@ -1,11 +1,6 @@
package io.xpipe.ext.system.incus;
import io.xpipe.app.ext.DataStoreState;
import io.xpipe.app.ext.FixedChildStore;
import io.xpipe.app.ext.FixedHierarchyStore;
import io.xpipe.app.ext.SelfReferentialStore;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.ext.StatefulDataStore;
import io.xpipe.app.ext.*;
import io.xpipe.app.process.CommandSupport;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.Validators;
@@ -34,6 +29,11 @@ public class IncusInstallStore
this.host = host;
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(host);
}
@Override
public void checkComplete() throws Throwable {
Validators.nonNull(host);
@@ -1,11 +1,6 @@
package io.xpipe.ext.system.lxd;
import io.xpipe.app.ext.DataStoreState;
import io.xpipe.app.ext.FixedChildStore;
import io.xpipe.app.ext.FixedHierarchyStore;
import io.xpipe.app.ext.SelfReferentialStore;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.ext.StatefulDataStore;
import io.xpipe.app.ext.*;
import io.xpipe.app.process.CommandSupport;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.Validators;
@@ -31,6 +26,11 @@ public class LxdCmdStore implements FixedHierarchyStore, StatefulDataStore<LxdCm
this.host = host;
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(host);
}
@Override
public void checkComplete() throws Throwable {
Validators.nonNull(host);
@@ -16,6 +16,7 @@ import lombok.Value;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
@@ -37,6 +38,11 @@ public class LxdContainerStore
String containerName;
IdentityValue identity;
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(cmd);
}
@Override
public String getName() {
return containerName;
@@ -30,6 +30,11 @@ public class PodmanCmdStore
this.host = host;
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(host);
}
@Override
public void checkComplete() throws Throwable {
Validators.nonNull(host);
@@ -41,6 +41,11 @@ public class PodmanContainerStore
DataStoreEntryRef<PodmanCmdStore> cmd;
String containerName;
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(cmd);
}
@Override
public String getName() {
return containerName;