2021-05-26 16:43:54 +02:00

173 lines
6.4 KiB
Java

package de.marhali.easyi18n.service;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.model.LocalizedNode;
import de.marhali.easyi18n.model.Translations;
import de.marhali.easyi18n.io.TranslatorIO;
import de.marhali.easyi18n.model.DataSynchronizer;
import de.marhali.easyi18n.model.KeyedTranslation;
import de.marhali.easyi18n.model.TranslationDelete;
import de.marhali.easyi18n.model.TranslationUpdate;
import de.marhali.easyi18n.util.IOUtil;
import de.marhali.easyi18n.util.TranslationsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.function.Consumer;
/**
* Factory service to manage localized messages for multiple projects at once.
* @author marhali
*/
public class DataStore {
private static final Map<Project, DataStore> INSTANCES = new WeakHashMap<>();
private final Project project;
private final List<DataSynchronizer> synchronizer;
private Translations translations;
private String searchQuery;
public static DataStore getInstance(@NotNull Project project) {
DataStore store = INSTANCES.get(project);
if(store == null) {
store = new DataStore(project);
INSTANCES.put(project, store);
}
return store;
}
private DataStore(@NotNull Project project) {
this.project = project;
this.synchronizer = new ArrayList<>();
this.translations = Translations.empty();
// Load data after first initialization
ApplicationManager.getApplication().invokeLater(this::reloadFromDisk, ModalityState.NON_MODAL);
}
/**
* Registers a new synchronizer which will receive {@link #translations} updates.
* @param synchronizer Synchronizer. See {@link DataSynchronizer}
*/
public void addSynchronizer(DataSynchronizer synchronizer) {
this.synchronizer.add(synchronizer);
}
/**
* Loads all translations from disk and overrides current {@link #translations} state.
*/
public void reloadFromDisk() {
String localesPath = SettingsService.getInstance(project).getState().getLocalesPath();
if(localesPath == null || localesPath.isEmpty()) {
// Propagate empty state
this.translations = Translations.empty();
synchronize(searchQuery, null);
} else {
TranslatorIO io = IOUtil.determineFormat(localesPath);
io.read(project, localesPath, (loadedTranslations) -> {
this.translations = loadedTranslations == null ? Translations.empty() : loadedTranslations;
synchronize(searchQuery, null);
});
}
}
/**
* Saves the current translation state to disk. See {@link TranslatorIO#save(Project, Translations, String, Consumer)}
* @param callback Complete callback. Indicates if operation was successful(true) or not
*/
public void saveToDisk(@NotNull Consumer<Boolean> callback) {
String localesPath = SettingsService.getInstance(project).getState().getLocalesPath();
if(localesPath == null || localesPath.isEmpty()) { // Cannot save without valid path
return;
}
TranslatorIO io = IOUtil.determineFormat(localesPath);
io.save(project, translations, localesPath, callback);
}
/**
* Propagates provided search string to all synchronizer to display only relevant keys
* @param fullPath Full i18n key (e.g. user.username.title). Can be null to display all keys
*/
public void searchBeyKey(@Nullable String fullPath) {
// Use synchronizer to propagate search instance to all views
synchronize(this.searchQuery = fullPath, null);
}
/**
* Processes the provided update. Updates translation instance and propagates changes. See {@link DataSynchronizer}
* @param update The update to process. For more information see {@link TranslationUpdate}
*/
public void processUpdate(TranslationUpdate update) {
if(update.isDeletion() || update.isKeyChange()) { // Delete origin i18n key
String originKey = update.getOrigin().getKey();
List<String> sections = TranslationsUtil.getSections(originKey);
String nodeKey = sections.remove(sections.size() - 1); // Remove last node, which needs to be removed by parent
LocalizedNode node = translations.getNodes();
for(String section : sections) {
if(node == null) { // Might be possible on multi-delete
break;
}
node = node.getChildren(section);
}
if(node != null) { // Only remove if parent exists. Might be already deleted on multi-delete
node.removeChildren(nodeKey);
// Parent is empty now, we need to remove it as well (except root)
if(node.getChildren().isEmpty() && !node.getKey().equals(LocalizedNode.ROOT_KEY)) {
processUpdate(new TranslationDelete(new KeyedTranslation(
TranslationsUtil.sectionsToFullPath(sections), null)));
}
}
}
String scrollTo = update.isDeletion() ? null : update.getChange().getKey();
if(!update.isDeletion()) { // Recreate with changed val / create
LocalizedNode node = translations.getOrCreateNode(update.getChange().getKey());
node.setValue(update.getChange().getTranslations());
}
// Persist changes and propagate them on success
saveToDisk(success -> {
if(success) {
synchronize(searchQuery, scrollTo);
}
});
}
/**
* @return Current translation state
*/
public @NotNull Translations getTranslations() {
return translations;
}
/**
* Synchronizes current translation's state to all connected subscribers.
* @param searchQuery Optional search by full key filter (ui view)
* @param scrollTo Optional scroll to full key (ui view)
*/
public void synchronize(@Nullable String searchQuery, @Nullable String scrollTo) {
synchronizer.forEach(subscriber -> subscriber.synchronize(this.translations, searchQuery, scrollTo));
}
}