
jdplus.toolkit.desktop.plugin.datatransfer.DataTransferManager Maven / Gradle / Ivy
/*
* Copyright 2018 National Bank of Belgium
*
* Licensed under the EUPL, Version 1.1 or - as soon they will be approved
* by the European Commission - subsequent versions of the EUPL (the "Licence");
* You may not use this work except in compliance with the Licence.
* You may obtain a copy of the Licence at:
*
* http://ec.europa.eu/idabc/eupl
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the Licence is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Licence for the specific language governing permissions and
* limitations under the Licence.
*/
package jdplus.toolkit.desktop.plugin.datatransfer;
import jdplus.toolkit.desktop.plugin.TsManager;
import jdplus.toolkit.desktop.plugin.beans.PropertyChangeSource;
import jdplus.main.desktop.design.GlobalService;
import jdplus.main.desktop.design.SwingProperty;
import jdplus.toolkit.desktop.plugin.util.CollectionSupplier;
import jdplus.toolkit.desktop.plugin.util.LazyGlobalService;
import jdplus.toolkit.base.api.timeseries.*;
import jdplus.toolkit.base.api.util.Table;
import nbbrd.design.swing.OnEDT;
import nbbrd.design.VisibleForTesting;
import nbbrd.io.function.IOFunction;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.awt.*;
import java.awt.datatransfer.*;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.util.List;
import java.util.*;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Objects.requireNonNull;
import jdplus.toolkit.base.api.math.matrices.Matrix;
/**
* A support class that deals with the clipboard. It allows the user to get/set
* time series, collections, matrices and tables from/to any transferable. The
* actual conversion is done by TssTransferHandler.
*
* @author Philippe Charles
*/
@lombok.extern.java.Log
@GlobalService
public final class DataTransferManager implements PropertyChangeSource.WithWeakListeners {
@NonNull
public static DataTransferManager get() {
return LazyGlobalService.get(DataTransferManager.class, DataTransferManager::new);
}
@SwingProperty
public static final String VALID_CLIPBOARD_PROPERTY = "validClipboard";
@lombok.experimental.Delegate(types = PropertyChangeSource.class)
private final PropertyChangeSupport broadcaster = new PropertyChangeSupport(this);
private final ClipboardValidator clipboardValidator;
private final CollectionSupplier providers;
private final Logger logger;
private boolean validClipboard;
private DataTransferManager() {
this(DataTransferSpiLoader::get, log, false);
clipboardValidator.register(Toolkit.getDefaultToolkit().getSystemClipboard());
}
@VisibleForTesting
DataTransferManager(CollectionSupplier providers, Logger logger, boolean validClipboard) {
this.clipboardValidator = new ClipboardValidator();
this.providers = providers;
this.logger = logger;
this.validClipboard = validClipboard;
}
private void setValidClipboard(boolean validClipboard) {
boolean old = this.validClipboard;
this.validClipboard = validClipboard;
broadcaster.firePropertyChange(VALID_CLIPBOARD_PROPERTY, old, this.validClipboard);
}
@NonNull
public Collection extends DataTransferSpi> getProviders() {
return providers.get();
}
@OnEDT
public boolean canImport(@NonNull DataFlavor... dataFlavors) {
// multiFlavor means "maybe", not "yes"
return DataTransfers.isMultiFlavor(dataFlavors) || providers.stream().anyMatch(onDataFlavors(dataFlavors));
}
@OnEDT
public boolean canImport(@NonNull Transferable transferable) {
Set dataFlavors = DataTransfers.getMultiDataFlavors(transferable).collect(Collectors.toSet());
return providers.stream().anyMatch(onDataFlavors(dataFlavors));
}
/**
* Creates a Transferable from a time series.
*
* @param ts a non-null {@link Ts}
* @return a never-null {@link Transferable}
*/
@OnEDT
@NonNull
public Transferable fromTs(@NonNull Ts ts) {
requireNonNull(ts);
return fromTsCollection(TsCollection.of(Collections.singletonList(ts)));
}
/**
* Creates a Transferable from a collection of time series.
*
* @param col a non-null {@link TsCollection}
* @return a never-null {@link Transferable}
*/
@OnEDT
@NonNull
public Transferable fromTsCollection(@NonNull TsCollection col) {
requireNonNull(col);
return asTransferable(col, providers.stream(), TsCollectionHelper.INSTANCE);
}
/**
* Creates a Transferable from a TsData.
*
* @param data a non-null {@link TsData}
* @return a never-null {@link Transferable}
*/
@OnEDT
@NonNull
public Transferable fromTsData(@NonNull TsData data) {
requireNonNull(data);
return fromTs(Ts.builder().moniker(TsMoniker.of()).data(data).build());
}
/**
* Creates a Transferable from a matrix.
*
* @param data a non-null {@link Matrix}
* @return a never-null {@link Transferable}
*/
@OnEDT
@NonNull
public Transferable fromMatrix(@NonNull Matrix data) {
requireNonNull(data);
return asTransferable(data, providers.stream(), MatrixHelper.INSTANCE);
}
/**
* Creates a Transferable from a Table.
*
* @param data a non-null {@link Table}
* @return a never-null {@link Transferable}
*/
@OnEDT
@NonNull
public Transferable fromTable(@NonNull Table> data) {
requireNonNull(data);
return asTransferable(data, providers.stream(), TableHelper.INSTANCE);
}
/**
* Checks if a {@link Transferable} represents time series (to avoid useless loading
* of Ts).
*
* @param transferable
* @return
*/
public boolean isTssTransferable(@NonNull Transferable transferable) {
return transferable.isDataFlavorSupported(LocalObjectDataTransfer.DATA_FLAVOR);
}
/**
* Checks if the clipboard currently contains data that can be imported.
*
* @return true if the data in the clipboard is importable; false otherwise
*/
@OnEDT
public boolean isValidClipboard() {
return validClipboard;
}
/**
* Retrieves a time series from a {@link Transferable}.
*
* Note that the content of this {@link Ts} might be asynchronous.
* Therefore, you should call {@link Ts#load(TsInformationType, TsFactory)} after this method.
*
* @param transferable a non-null object
* @return an optional {@link Ts}
*/
@OnEDT
@NonNull
public Optional toTs(@NonNull Transferable transferable) {
return toTsCollection(transferable)
.map(TsCollection::getItems)
.filter(o -> !o.isEmpty())
.map(o -> o.get(0));
}
/**
* Retrieves a collection of time series from a {@link Transferable}.
*
* Note that the content of this {@link TsCollection} might be asynchronous.
* Therefore, you should call
* {@link TsCollection#load(TsInformationType, TsFactory)} after this method.
*
* @param transferable a non-null object
* @return an optional {@link TsCollection}
*/
@OnEDT
@NonNull
public Optional toTsCollection(@NonNull Transferable transferable) {
requireNonNull(transferable);
return providers.stream()
.filter(onDataFlavors(transferable.getTransferDataFlavors()))
.map(o -> getTsCollectionOrNull(o, transferable, logger))
.filter(Objects::nonNull)
.findFirst();
}
@OnEDT
@NonNull
public Stream toTsCollectionStream(@NonNull Transferable transferable) {
return DataTransfers.getMultiTransferables(transferable)
.map(this::toTsCollection)
.filter(Optional::isPresent)
.map(Optional::orElseThrow);
}
/**
* Retrieves a TsData from a transferable.
*
* @param transferable a non-null object
* @return an optional {@link TsData}
*/
@OnEDT
@NonNull
public Optional toTsData(@NonNull Transferable transferable) {
return toTs(transferable)
.map(ts -> ts.load(TsInformationType.Data, TsManager.get()).getData());
}
/**
* Retrieves a matrix from a transferable.
*
* @param transferable a non-null object
* @return an optional {@link Matrix}
*/
@OnEDT
@NonNull
public Optional toMatrix(@NonNull Transferable transferable) {
return providers.stream()
.filter(onDataFlavors(transferable.getTransferDataFlavors()))
.map(o -> getMatrixOrNull(o, transferable, logger))
.filter(Objects::nonNull)
.findFirst();
}
/**
* Retrieves a Table from a transferable.
*
* @param transferable a non-null object
* @return an optional {@link Table}
*/
@OnEDT
@NonNull
public Optional> toTable(@NonNull Transferable transferable) {
Predicate predicate = onDataFlavors(transferable.getTransferDataFlavors());
for (DataTransferSpi o : providers.get()) {
if (predicate.test(o)) {
Table> table = getTableOrNull(o, transferable, logger);
if (table != null) {
return Optional.of(table);
}
}
}
return Optional.empty();
}
private final class ClipboardValidator implements FlavorListener {
private boolean isValid(Clipboard clipboard) {
try {
return canImport(clipboard.getAvailableDataFlavors());
} catch (IllegalStateException ex) {
logger.log(Level.FINE, "While getting content from clipboard", ex);
return true; // means "maybe", not "yes"
}
}
@Override
public void flavorsChanged(FlavorEvent e) {
setValidClipboard(isValid((Clipboard) e.getSource()));
}
public void register(Clipboard clipboard) {
clipboard.addFlavorListener(this);
validClipboard = isValid(clipboard);
}
}
private static void logUnexpected(Logger logger, DataTransferSpi o, RuntimeException unexpected, String context) {
logger.log(Level.INFO, "Unexpected exception while " + context + " using '" + getIdOrClassName(o) + "'", unexpected);
}
private static void logExpected(Logger logger, DataTransferSpi o, Exception expected, String context) {
logger.log(Level.FINE, "While " + context + " using '" + getIdOrClassName(o) + "'", expected);
}
private static String getIdOrClassName(DataTransferSpi handler) {
try {
return requireNonNull(handler.getName());
} catch (RuntimeException unexpected) {
return handler.getClass().getName();
}
}
private static DataFlavor getDataFlavorOrNull(DataTransferSpi handler) {
try {
return handler.getDataFlavor();
} catch (RuntimeException unexpected) {
return null;
}
}
private static TsCollection getTsCollectionOrNull(DataTransferSpi o, Transferable t, Logger logger) {
try {
Object data = t.getTransferData(requireNonNull(o.getDataFlavor()));
if (o.canImportTsCollection(data)) {
return requireNonNull(o.importTsCollection(data));
}
} catch (UnsupportedFlavorException | IOException ex) {
logExpected(logger, o, ex, "getting collection");
} catch (RuntimeException ex) {
logUnexpected(logger, o, ex, "getting collection");
}
return null;
}
private static Matrix getMatrixOrNull(DataTransferSpi o, Transferable t, Logger logger) {
try {
Object data = t.getTransferData(requireNonNull(o.getDataFlavor()));
if (o.canImportMatrix(data)) {
return requireNonNull(o.importMatrix(data));
}
} catch (UnsupportedFlavorException | IOException ex) {
logExpected(logger, o, ex, "getting matrix");
} catch (RuntimeException ex) {
logUnexpected(logger, o, ex, "getting matrix");
}
return null;
}
private static Table> getTableOrNull(DataTransferSpi o, Transferable t, Logger logger) {
try {
Object data = t.getTransferData(requireNonNull(o.getDataFlavor()));
if (o.canImportTable(data)) {
return requireNonNull(o.importTable(data));
}
} catch (UnsupportedFlavorException | IOException ex) {
logExpected(logger, o, ex, "getting table");
} catch (RuntimeException ex) {
logUnexpected(logger, o, ex, "getting table");
}
return null;
}
@OnEDT
private static Transferable asTransferable(T data, Stream extends DataTransferSpi> allHandlers, TypeHelper helper) {
return new MultiTransferable<>(
getHandlersByFlavor(data, allHandlers, helper),
getTransferDataLoader(data, helper));
}
private static Map> getHandlersByFlavor(T data, Stream extends DataTransferSpi> allHandlers, TypeHelper helper) {
return allHandlers
.filter(o -> helper.canTransferData(data, o))
.collect(Collectors.groupingBy(DataTransferManager::getDataFlavorOrNull));
}
private static IOFunction getTransferDataLoader(T data, TypeHelper helper) {
return o -> helper.getTransferData(data, o);
}
private interface TypeHelper {
boolean canTransferData(T data, DataTransferSpi handler);
Object getTransferData(T data, DataTransferSpi handler) throws IOException;
}
private static final class TsCollectionHelper implements TypeHelper {
private static final TsCollectionHelper INSTANCE = new TsCollectionHelper();
@Override
public boolean canTransferData(TsCollection data, DataTransferSpi handler) {
return handler.canExportTsCollection(data);
}
@Override
public Object getTransferData(TsCollection data, DataTransferSpi handler) throws IOException {
return handler.exportTsCollection(data);
}
}
private static final class MatrixHelper implements TypeHelper {
private static final MatrixHelper INSTANCE = new MatrixHelper();
@Override
public boolean canTransferData(Matrix data, DataTransferSpi handler) {
return handler.canExportMatrix(data);
}
@Override
public Object getTransferData(Matrix data, DataTransferSpi handler) throws IOException {
return handler.exportMatrix(data);
}
}
private static final class TableHelper implements TypeHelper> {
private static final TableHelper INSTANCE = new TableHelper();
@Override
public boolean canTransferData(Table> data, DataTransferSpi handler) {
return handler.canExportTable(data);
}
@Override
public Object getTransferData(Table> data, DataTransferSpi handler) throws IOException {
return handler.exportTable(data);
}
}
private static Predicate onDataFlavors(DataFlavor[] dataFlavors) {
return onDataFlavors(new HashSet<>(Arrays.asList(dataFlavors)));
}
private static Predicate onDataFlavors(Set dataFlavors) {
return o -> dataFlavors.contains(o != null ? getDataFlavorOrNull(o) : null);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy