package org.openslx.dozmod.gui.window;
import java.awt.Color;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import org.apache.log4j.Logger;
import org.apache.thrift.TException;
import org.openslx.bwlp.thrift.iface.ImageBaseWrite;
import org.openslx.bwlp.thrift.iface.ImageDetailsRead;
import org.openslx.bwlp.thrift.iface.ImagePermissions;
import org.openslx.bwlp.thrift.iface.ImageSummaryRead;
import org.openslx.bwlp.thrift.iface.ImageVersionDetails;
import org.openslx.bwlp.thrift.iface.LectureSummary;
import org.openslx.bwlp.thrift.iface.OperatingSystem;
import org.openslx.bwlp.thrift.iface.ShareMode;
import org.openslx.bwlp.thrift.iface.UserInfo;
import org.openslx.bwlp.thrift.iface.Virtualizer;
import org.openslx.dozmod.gui.Gui;
import org.openslx.dozmod.gui.MainWindow;
import org.openslx.dozmod.gui.helper.MessageType;
import org.openslx.dozmod.gui.helper.PopupMenu;
import org.openslx.dozmod.gui.helper.TextChangeListener;
import org.openslx.dozmod.gui.helper.UiFeedback;
import org.openslx.dozmod.gui.window.UserListWindow.UserAddedCallback;
import org.openslx.dozmod.gui.window.layout.ImageDetailsWindowLayout;
import org.openslx.dozmod.gui.wizard.ImageUpdateWizard;
import org.openslx.dozmod.gui.wizard.LectureWizard;
import org.openslx.dozmod.permissions.ImagePerms;
import org.openslx.dozmod.thrift.Session;
import org.openslx.dozmod.thrift.ThriftActions;
import org.openslx.dozmod.thrift.ThriftActions.DeleteCallback;
import org.openslx.dozmod.thrift.ThriftActions.ImageMetaCallback;
import org.openslx.dozmod.thrift.ThriftError;
import org.openslx.dozmod.thrift.cache.LectureCache;
import org.openslx.dozmod.thrift.cache.MetaDataCache;
import org.openslx.dozmod.thrift.cache.UserCache;
import org.openslx.dozmod.util.FormatHelper;
import org.openslx.dozmod.util.MapHelper;
/**
* Window for displaying and editing the details of an image.
*/
@SuppressWarnings("serial")
public class ImageDetailsWindow extends ImageDetailsWindowLayout implements UiFeedback {
private static final Logger LOGGER = Logger.getLogger(ImageDetailsWindow.class);
/**
* Self-reference
*/
private final ImageDetailsWindow me = this;
/**
* Callback interface to refresh image list after changing image details
*/
public interface ImageUpdatedCallback {
public void updated();
}
/**
* Callback instance
*/
private ImageUpdatedCallback callback = null;
/**
* Image that this window shows the details of
*/
private ImageDetailsRead image = null;
/**
* The current state of custom permissions of the image
*/
private Map<String, ImagePermissions> customPermissions;
/**
* The original custom permissions as fetched from the server
*/
private Map<String, ImagePermissions> originalCustomPermissions;
/**
* The original default permissions as fetched from the server
*/
private ImagePermissions originalDefaultPermissions;
/**
* Popup menu items
*/
private final JMenuItem mnuNewLecture = new JMenuItem("Neue Veranstaltung");
private final JMenuItem mnuDownload = new JMenuItem("Download");
private final JMenuItem mnuDelete = new JMenuItem("Löschen");
private boolean metadataChanged = false;
private boolean permissionsChanged = false;
/**
* Constructor
*
* @param modalParent parent of this popup window
* @param callback callback to be called when the image details have changed
*/
public ImageDetailsWindow(Frame modalParent, ImageUpdatedCallback callback) {
super(modalParent);
this.callback = callback;
// Hook when user presses X (top right)
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
safeClose();
}
});
/**
* Button listeners
*/
btnClose.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
safeClose();
}
});
btnSaveChanges.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
saveChanges();
}
});
btnUpdateImage.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
new ImageUpdateWizard(me, image).setVisible(true);
refresh(true);
}
});
btnShowLinkingLectures.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (safeClose()) {
LectureListWindow page = MainWindow.showPage(LectureListWindow.class);
page.filterByImageBaseId(image.imageBaseId);
}
}
});
btnChangeOwner.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
UserListWindow.open(me, new UserAddedCallback() {
@Override
public void userAdded(UserInfo user, UserListWindow window) {
window.dispose();
if (Gui.showMessageBox(me, "Sind Sie sicher, dass sie die Besitzerrechte an "
+ "einen anderen Benutzer übertragen wollen?", MessageType.QUESTION_YESNO,
LOGGER, null))
setImageOwner(user);
}
}, "Besitzer festlegen", image.ownerId);
}
});
btnPermissions.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
// open the custom permission window and save returned default/custom permissions
ImagePermissionWindow.open(me, customPermissions, image.defaultPermissions, image.ownerId);
// since the window above gets references of the default/custom permission object
// there is no need to further save the return value ...
// THAT or we do work with copies in ImagePermissionWindow ...
// for now let's stay with refs and just reactToChange() where we check if
// the permissions stuff changed since the last call of fill()
reactToChange();
}
});
tblVersions.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
/**
* Popup menu for the version table on the right side
*/
final PopupMenu pop = new PopupMenu(new ActionListener() {
public void actionPerformed(ActionEvent e) {
ImageVersionDetails selected = tblVersions.getSelectedItem();
if (selected == null)
return;
if (e.getSource().equals(mnuNewLecture)) {
ImageSummaryRead summary = new ImageSummaryRead();
summary.setImageName(image.getImageName()); // Maybe create a helper class/function some day that transforms all fields
new LectureWizard(me, summary, selected.getVersionId()).setVisible(true);
}
if (e.getSource().equals(mnuDownload)) {
performImageDownload(selected);
}
if (e.getSource().equals(mnuDelete)) {
deleteVersions(tblVersions.getSelectedItems());
}
}
});
pop.addMenuItem(mnuNewLecture);
pop.addMenuItem(mnuDownload);
pop.addSeparator();
pop.addMenuItem(mnuDelete);
// keyboard shortcut
tblVersions.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete");
tblVersions.getActionMap().put("delete", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent ae) {
if (ImagePerms.canEdit(image)) {
deleteVersions(tblVersions.getSelectedItems());
}
}
});
/**
* Mouse adapter for the version table
*/
final MouseAdapter ma = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
processClick(e);
}
@Override
public void mouseReleased(MouseEvent e) {
processClick(e);
}
private void processClick(MouseEvent e) {
// rowIndex at mouse cursor
int mouseRowIndex = tblVersions.rowAtPoint(e.getPoint());
// is the click event on an already selected row?
boolean alreadySelectedRow = false;
for (int i : tblVersions.getSelectedRows()) {
if (i == mouseRowIndex) {
alreadySelectedRow = true;
break;
}
}
if (mouseRowIndex >= 0 && mouseRowIndex < tblVersions.getRowCount()
&& SwingUtilities.isRightMouseButton(e)) {
// select row if it wasn't in selection before
if (!alreadySelectedRow) {
tblVersions.setRowSelectionInterval(mouseRowIndex, mouseRowIndex);
}
if (e.isPopupTrigger()) {
boolean multiSelection = tblVersions.getSelectedRowCount() != 1;
mnuNewLecture.setEnabled(tblVersions.getSelectedItem().isValid
&& ImagePerms.canLink(image) && !multiSelection);
mnuDownload.setEnabled(tblVersions.getSelectedItem().isValid
&& ImagePerms.canDownload(image) && !multiSelection);
mnuDelete.setEnabled(tblVersions.getSelectedItem().isValid
&& ImagePerms.canEdit(image));
pop.show(e.getComponent(), e.getX(), e.getY());
}
}
}
};
scpVersions.addMouseListener(ma);
tblVersions.addMouseListener(ma);
// listen to changes
final TextChangeListener docListener = new TextChangeListener() {
@Override
public void changed() {
reactToChange();
}
};
final ItemListener comboItemListener = new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if (e.getStateChange() == ItemEvent.SELECTED) {
reactToChange();
}
}
};
final ActionListener checkBoxListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
reactToChange();
}
};
txtTitle.getDocument().addDocumentListener(docListener);
txtDescription.getDocument().addDocumentListener(docListener);
txtTags.getDocument().addDocumentListener(docListener);
cboOperatingSystem.addItemListener(comboItemListener);
cboShareMode.addItemListener(comboItemListener);
chkIsTemplate.addActionListener(checkBoxListener);
/**
* Initial state of GUI elements
*/
setFocusable(true);
btnSaveChanges.setEnabled(false);
txtVersion.setEditable(false);
txtId.setEditable(false);
chkIsTemplate.setEnabled(Session.isSuperUser());
makeEditable(false);
// TODO finish ShareMode in server
cboShareMode.setEnabled(false);
}
/********************************************************************************
*
* Helper triggering the actual thrift calls
*
********************************************************************************/
/**
* Sets the image to the given imageBaseId. This will also trigger fill()
* which will set the image details fields to the values represented by this
* image.
*
* @param imageBaseId the id of the image to be displayed
*/
public void setImage(final String imageBaseId) {
// Just prime the cache...
MetaDataCache.getOperatingSystems();
MetaDataCache.getVirtualizers();
final ImageMetaCallback callback = new ImageMetaCallback() {
@Override
public void fetchedImageDetails(ImageDetailsRead imageDetails,
Map<String, ImagePermissions> permissions) {
if (imageDetails == null) {
return;
}
if (permissions == null) {
permissions = new HashMap<>();
}
synchronized (me) {
image = imageDetails;
customPermissions = permissions;
}
fillDetails();
}
};
ThriftActions.getImageFullDetails(JOptionPane.getFrameForComponent(me), imageBaseId, callback);
}
/**
* Sets the owner of the selected image to the given user.
*
* @param user UserInfo to set the owner to
*/
private void setImageOwner(final UserInfo user) {
if (!ThriftActions.setImageOwner(JOptionPane.getFrameForComponent(me), image.getImageBaseId(), user)) {
return;
}
Gui.showMessageBox(me, "Besitzrechte übertragen an " + FormatHelper.userName(user), MessageType.INFO,
null, null);
makeEditable(false);
refresh(true);
}
/**
* Called by the "Save" button, tries to save the changes internally and
* then react based depending on the outcome of the save
*/
private void saveChanges() {
boolean saved = saveChangesInternal();
// if there was nothing to save, saved would be true from the above call
// however we wouldn't even get to saving if nothing was changed, so its fine
if (saved) {
if (callback != null)
callback.updated();
dispose();
} else {
btnSaveChanges.setEnabled(true);
}
}
/**
* Helper to only save the changes, nothing else. Updating GUI elements is
* done by saveChanges()
*
* @return false if any try to save changes failed, true otherwise
*/
private boolean saveChangesInternal() {
// We might only have admin perms because of the default permissions,
// so we first write the permission map because we might remove our own default admin permissions.
if (originalDefaultPermissions.admin && permissionsChanged) {
if (!writeImagePermMap())
return false;
LOGGER.info("Successfully saved new permissions");
permissionsChanged = false;
}
// now trigger the actual action
if (metadataChanged) {
// first build the ImageBaseWrite from the GUI fields
final ImageBaseWrite ibw = new ImageBaseWrite(txtTitle.getText(), txtDescription.getText(),
cboOperatingSystem.getItemAt(cboOperatingSystem.getSelectedIndex()).osId, image.virtId,
chkIsTemplate.isSelected(), new ImagePermissions(image.defaultPermissions.link,
image.defaultPermissions.download, image.defaultPermissions.edit,
image.defaultPermissions.admin),
cboShareMode.getItemAt(cboShareMode.getSelectedIndex()));
try {
ThriftActions.updateImageBase(image.getImageBaseId(), ibw);
} catch (TException e) {
ThriftError.showMessage(me, LOGGER, e,
"Konnte aktualisierte Metadaten nicht an den Server übermitteln");
return false;
}
LOGGER.info("Successfully saved new metadata");
metadataChanged = false;
}
// if we haven't written the permission map yet and the permissions have changed
if (permissionsChanged) {
if (!writeImagePermMap())
return false;
LOGGER.info("Successfully saved new permissions");
permissionsChanged = false;
}
return true;
}
private boolean writeImagePermMap() {
try {
ThriftActions.writeImagePermissions(image.getImageBaseId(), customPermissions);
} catch (TException e) {
ThriftError.showMessage(me, LOGGER, e,
"Konnte geänderte Berechtigungen nicht an den Server übermitteln");
return false;
}
return true;
}
/**
* Triggers the download of the given image version
*
* @param selected image to download
*/
private void performImageDownload(ImageVersionDetails selected) {
if (selected.getVersionId() == null) {
Gui.showMessageBox(this, "Ausgewählte Version ist ungültig", MessageType.ERROR, null, null);
return;
}
ThriftActions.initDownload(JOptionPane.getFrameForComponent(this), selected.versionId,
image.imageName, image.virtId, image.osId, selected.fileSize, null);
}
/**
* Triggers the deletion of the given image version
*
* @param version image version to delete
*/
private void deleteVersion(final ImageVersionDetails version) {
if (version == null)
return;
ThriftActions.deleteImageVersion(JOptionPane.getFrameForComponent(this), version,
new DeleteCallback() {
@Override
public void isDeleted(boolean success) {
refresh(success);
}
});
}
/**
* Triggers the deletion of a list of versions
*
* @param versions list of versions to delete
*/
private void deleteVersions(List<ImageVersionDetails> versions) {
if (versions == null || versions.isEmpty())
return;
for (ImageVersionDetails version : versions) {
deleteVersion(version);
}
}
/********************************************************************************
*
* General UI helpers
*
********************************************************************************/
/**
* @param forceRefresh
*/
private void refresh(boolean forceRefresh) {
String baseId = image.getImageBaseId();
synchronized (me) {
image = null;
}
setImage(baseId);
}
/**
* callback function when we received the image's details from the server
*/
private void fillDetails() {
if (image == null)
return;
// remember default permissions
if (image.defaultPermissions != null) {
originalDefaultPermissions = new ImagePermissions(image.defaultPermissions);
}
// remember custom permissions
if (customPermissions != null) {
// need a deep copy of the permission map to be able to check for
// changes after ImageCustomPermissionWindow
if (originalCustomPermissions == null)
originalCustomPermissions = new HashMap<String, ImagePermissions>();
else
originalCustomPermissions.clear();
// fill it
for (Entry<String, ImagePermissions> entry : customPermissions.entrySet()) {
originalCustomPermissions.put(entry.getKey(), new ImagePermissions(entry.getValue()));
}
}
txtTitle.setText(image.getImageName());
txtDescription.setText(image.getDescription());
lblOwner.setUser(UserCache.find(image.getOwnerId()));
lblUpdater.setUser(UserCache.find(image.getUpdaterId()));
lblCreateTime.setText(FormatHelper.longDate(image.getCreateTime()));
lblUpdateTime.setText(FormatHelper.longDate(image.getUpdateTime()));
txtVersion.setText(image.getLatestVersionId());
txtId.setText(image.getImageBaseId());
chkIsTemplate.setSelected(image.isTemplate);
setTitle(image.getImageName());
// fill os combo, but only once :)
if (cboOperatingSystem.getItemCount() == 0) {
List<OperatingSystem> osList = MetaDataCache.getOperatingSystems();
// all fine, lets sort it
Collections.sort(osList, new Comparator<OperatingSystem>() {
public int compare(OperatingSystem o1, OperatingSystem o2) {
return o1.getOsName().compareTo(o2.getOsName());
}
});
for (OperatingSystem os : osList) {
cboOperatingSystem.addItem(os);
}
cboOperatingSystem.setSelectedItem(new OperatingSystem(image.getOsId(), null, null, null, 0, 0));
}
Virtualizer virt = MetaDataCache.getVirtualizerById(image.getVirtId(), true);
if (virt != null)
lblVirtualizer.setText(virt.getVirtName());
// fill share mode combo, if not already done
if (cboShareMode.getItemCount() == 0) {
for (ShareMode mode : ShareMode.values()) {
cboShareMode.addItem(mode);
}
cboShareMode.setSelectedItem(image.getShareMode());
}
String tagsString = "";
for (String tag : image.getTags()) {
tagsString = tagsString + ", " + tag;
}
txtTags.setText(tagsString);
// Count the number of linked lectures to the image
int lectureCount = 0;
for (LectureSummary lecture : LectureCache.get(false)) {
if (lecture == null || lecture.imageBaseId == null)
continue;
if (lecture.imageBaseId.equals(image.imageBaseId))
lectureCount++;
}
lblLinkedLectureCount.setText(Integer.toString(lectureCount));
lblLinkedLectureCount.setForeground(lectureCount > 0 ? null : Color.RED);
btnShowLinkingLectures.setEnabled(lectureCount > 0);
// set the versions of the image to the table
tblVersions.setData(image.getVersions(), true);
mnuDelete.setEnabled(ImagePerms.canAdmin(image));
mnuDownload.setEnabled(ImagePerms.canDownload(image));
mnuNewLecture.setEnabled(ImagePerms.canAdmin(image));
// make fields editable is allowed
makeEditable(true);
// finally do show it all
setVisible(true);
}
/**
* Checks whether the user changed any fields of the image details and
* enables the save button if so TODO TAGS
*/
private void reactToChange() {
metadataChanged = reactToChangeInternal();
permissionsChanged = MapHelper.hasChanged(originalCustomPermissions, customPermissions);
btnSaveChanges.setEnabled(metadataChanged || permissionsChanged);
}
private boolean reactToChangeInternal() {
if (image == null)
return false;
boolean changed = false;
// Image name
if (txtTitle.getText().isEmpty()) {
lblError.setText("Kein VM-Name gesetzt!");
return false;
}
// Image description
if (txtDescription.getText().isEmpty()) {
lblError.setText("Keine Beschreibung angegeben!");
return false;
}
// Operating system
OperatingSystem newOs = cboOperatingSystem.getItemAt(cboOperatingSystem.getSelectedIndex());
if (newOs == null) {
lblError.setText("Kein Betriebssystem ausgewählt!");
return false;
}
// Share mode
ShareMode newShareMode = cboShareMode.getItemAt(cboShareMode.getSelectedIndex());
if (newShareMode == null) {
lblError.setText("Kein Share-Modus ausgewählt!");
return false;
}
// mandatory checks done, remove error message
lblError.setText(null);
// Template, default permissions and custom permissions:
// no sanity checks here only check if they changed
changed = !txtTitle.getText().equals(image.getImageName())
|| !txtDescription.getText().equals(image.getDescription())
|| newOs.getOsId() != image.getOsId() || !newShareMode.equals(image.shareMode)
|| chkIsTemplate.isSelected() != image.isTemplate
|| !image.defaultPermissions.equals(originalDefaultPermissions);
return changed;
}
/**
* Enables/disables the editable fields based on 'editable'
*
* @param editable true to make fields editable, false otherwise.
*/
private void makeEditable(boolean editable) {
editable = editable && (ImagePerms.canEdit(image) || ImagePerms.canAdmin(image));
txtTitle.setEditable(editable);
txtDescription.setEditable(editable);
txtTags.setEditable(editable);
cboOperatingSystem.setEnabled(editable);
// cboShareMode.setEnabled(editable);
btnPermissions.setEnabled(editable && ImagePerms.canAdmin(image));
btnChangeOwner.setEnabled(editable && ImagePerms.canAdmin(image));
btnUpdateImage.setEnabled(editable);
}
/**
* Opens a new ImageDetailsWindow showing the details of the image with ID =
* imageBaseId
*
* @param modalParent parent of this window
* @param imageBaseId id of the image to set the details of
*/
public static void open(Frame modalParent, String imageBaseId, ImageUpdatedCallback callback) {
ImageDetailsWindow win = new ImageDetailsWindow(modalParent, callback);
win.setImage(imageBaseId);
win.setVisible(true);
}
/* *******************************************************************************
*
* Dialog class overrides
*
* *******************************************************************************
*/
@SuppressWarnings("deprecation")
@Override
public void show() {
if (!isVisible()) {
pack();
MainWindow.centerShell(this);
}
super.show();
}
/* *******************************************************************************
*
* UIFeedback implementation
*
* *******************************************************************************
*/
@Override
public boolean wantConfirmQuit() {
return metadataChanged || permissionsChanged;
}
@Override
public void escapePressed() {
safeClose();
}
/*
* Safe close helper: checks if we have unsaved work and prompt the user for
* confirmation if so
*/
private boolean safeClose() {
if ((metadataChanged || permissionsChanged)
&& !Gui.showMessageBox(me, "Änderungen werden verworfen, wollen Sie wirklich schließen?",
MessageType.QUESTION_YESNO, null, null))
return false;
dispose();
return true;
}
}