/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.esa.snap.rcp.actions.file;
import org.esa.snap.core.dataio.ProductReader;
import org.esa.snap.core.datamodel.Band;
import org.esa.snap.core.datamodel.Product;
import org.esa.snap.core.datamodel.ProductNode;
import org.esa.snap.core.datamodel.RasterDataNode;
import org.esa.snap.core.datamodel.VirtualBand;
import org.esa.snap.core.jexp.ParseException;
import org.esa.snap.netbeans.docwin.DocumentWindow;
import org.esa.snap.netbeans.docwin.DocumentWindowManager;
import org.esa.snap.netbeans.docwin.WindowUtilities;
import org.esa.snap.rcp.SnapApp;
import org.esa.snap.rcp.util.Dialogs;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionReferences;
import org.openide.awt.ActionRegistration;
import org.openide.util.ContextAwareAction;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;
import org.openide.util.WeakListeners;
import org.openide.util.WeakSet;
import javax.swing.AbstractAction;
import javax.swing.Action;
import java.awt.event.ActionEvent;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Action which closes a selected product.
*
* @author Norman
*/
@ActionID(
category = "File",
id = "CloseProductAction"
)
@ActionRegistration(
displayName = "#CTL_CloseProductActionName", lazy = false
)
@ActionReferences({
@ActionReference(path = "Menu/File", position = 20, separatorBefore = 18),
@ActionReference(path = "Context/Product/Product", position = 60)
})
@NbBundle.Messages({
"CTL_CloseProductActionName=Close Product"
})
public final class CloseProductAction extends AbstractAction implements ContextAwareAction, LookupListener {
private final WeakSet<Product> productSet = new WeakSet<>();
private Lookup lkp;
public CloseProductAction() {
this(Utilities.actionsGlobalContext());
}
public CloseProductAction(Lookup actionContext) {
super(Bundle.CTL_CloseProductActionName());
this.lkp = actionContext;
Lookup.Result<ProductNode> productNode = lkp.lookupResult(ProductNode.class);
productNode.addLookupListener(WeakListeners.create(LookupListener.class, this, productNode));
setEnableState();
setActionName();
}
public CloseProductAction(List<Product> products) {
productSet.addAll(products);
}
@Override
public Action createContextAwareInstance(Lookup actionContext) {
return new CloseProductAction(actionContext);
}
@Override
public void resultChanged(LookupEvent lookupEvent) {
setEnableState();
setActionName();
}
private Set<Product> getSelectedProducts() {
Collection<? extends ProductNode> selectedNodes = lkp.lookupAll(ProductNode.class);
return selectedNodes.stream().map(ProductNode::getProduct).collect(Collectors.toSet());
}
private void setActionName() {
Set<Product> selectedProducts = getSelectedProducts();
if (selectedProducts.size() > 1) {
this.putValue(Action.NAME, String.format("Close %d Products", selectedProducts.size()));
} else {
this.putValue(Action.NAME, "Close Product");
}
}
@Override
public void actionPerformed(ActionEvent e) {
execute();
}
private void setEnableState() {
setEnabled(lkp.lookup(ProductNode.class) != null);
}
/**
* Executes the action command.
*
* @return {@code Boolean.TRUE} on success, {@code Boolean.FALSE} on failure, or {@code null} on cancellation.
*/
public Boolean execute() {
Boolean status;
if (!productSet.isEmpty()) {
// Case 1: If productSet is not empty, action has been constructed with selected products
status = closeProducts(productSet);
productSet.clear();
} else {
// Case 2: If productSet is empty, default constructor has been called
status = closeProducts(getSelectedProducts());
}
return status;
}
public static Boolean closeProducts(Set<Product> products) {
List<Product> closeList = new ArrayList<>(products);
List<Product> saveList = new ArrayList<>();
HashSet<Product> stillOpenProducts = new HashSet<>(Arrays.asList(SnapApp.getDefault().getProductManager().getProducts()));
stillOpenProducts.removeAll(closeList);
if (!stillOpenProducts.isEmpty()) {
for (Product productToBeClosed : closeList) {
Product firstSourceProduct = findFirstSourceProduct(productToBeClosed, stillOpenProducts);
if (firstSourceProduct != null) {
Dialogs.showInformation("Close Not Possible",
String.format("Can't close product '%s' because it is in use%n" +
"by product '%s'.%n" +
"Please close the latter first.",
productToBeClosed.getName(),
firstSourceProduct.getName()), null);
return false;
}
}
}
for (Product product : products) {
if (product.isModified()) {
Dialogs.Answer answer = Dialogs.requestDecision(Bundle.CTL_OpenProductActionName(),
MessageFormat.format("Product ''{0}'' has been modified.\n" +
"Do you want to save it?",
product.getName()), true, null);
if (answer == Dialogs.Answer.YES) {
saveList.add(product);
} else if (answer == Dialogs.Answer.CANCELLED) {
return null;
}
}
}
for (Product product : saveList) {
Boolean status = new SaveProductAction(product).execute();
if (status == null) {
// cancelled
return null;
}
}
for (Product product : closeList) {
WindowUtilities.getOpened(DocumentWindow.class)
.filter(dw -> (dw.getDocument() instanceof ProductNode) && ((ProductNode) dw.getDocument()).getProduct() == product)
.forEach(dw -> DocumentWindowManager.getDefault().closeWindow(dw));
SnapApp.getDefault().getProductManager().removeProduct(product);
}
closeList.forEach(Product::dispose);
return true;
}
static Product findFirstSourceProduct(Product productToClose, Set<Product> productsStillOpen) {
Product firstSourceProduct = findFirstDirectSourceProduct(productToClose, productsStillOpen);
if (firstSourceProduct != null) {
return firstSourceProduct;
}
return findFirstExpressionSourceProduct(productToClose, productsStillOpen);
}
private static Product findFirstDirectSourceProduct(Product productToBeClosed, Set<Product> productsStillOpen) {
for (Product openProduct : productsStillOpen) {
final ProductReader reader = openProduct.getProductReader();
if (reader != null) {
final Object input = reader.getInput();
if (input instanceof Product) {
Product sourceProduct = (Product) input;
if (productToBeClosed.equals(sourceProduct)) {
return openProduct;
} else {
Product indirectSourceProduct = findFirstDirectSourceProduct(sourceProduct, productsStillOpen);
if (indirectSourceProduct != null && productToBeClosed.equals(indirectSourceProduct)) {
return openProduct;
}
}
} else {
if (input instanceof Product[]) {
for (final Product sourceProduct : (Product[]) input) {
if (productToBeClosed.equals(sourceProduct)) {
return openProduct;
}
Product indirectSourceProduct = findFirstDirectSourceProduct(sourceProduct, productsStillOpen);
if (indirectSourceProduct != null && productToBeClosed.equals(indirectSourceProduct)) {
return openProduct;
}
}
}
}
}
}
return null;
}
static Product findFirstExpressionSourceProduct(Product productToBeClosed, Set<Product> productsStillOpen) {
for (Product openProduct : productsStillOpen) {
Band[] bands = openProduct.getBands();
for (Band band : bands) {
if (band instanceof VirtualBand) {
VirtualBand virtualBand = (VirtualBand) band;
try {
RasterDataNode[] nodes = openProduct.getRefRasterDataNodes(virtualBand.getExpression());
for (RasterDataNode node : nodes) {
if (productToBeClosed.equals(node.getProduct())) {
return openProduct;
}
}
} catch (ParseException e) {
// ok
}
}
}
}
return null;
}
}