package org.openslx.dozmod.gui.wizard.page;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.openslx.bwlp.thrift.iface.ImageDetailsRead;
import org.openslx.dozmod.Config;
import org.openslx.dozmod.gui.Gui;
import org.openslx.dozmod.gui.helper.I18n;
import org.openslx.dozmod.gui.helper.MessageType;
import org.openslx.dozmod.gui.helper.QFileChooser;
import org.openslx.dozmod.gui.helper.TextChangeListener;
import org.openslx.dozmod.gui.wizard.Wizard;
import org.openslx.dozmod.gui.wizard.layout.ContainerUploadPageLayout;
import org.openslx.dozmod.model.ContainerDefinition;
import org.openslx.dozmod.state.UploadWizardState;
import org.openslx.dozmod.thrift.*;
import org.openslx.dozmod.thrift.cache.MetaDataCache;
import org.openslx.dozmod.util.ContainerUtils;
import org.openslx.virtualization.configuration.VirtualizationConfigurationDocker;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.filechooser.FileFilter;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ContainerUploadPage extends ContainerUploadPageLayout {
// TODO: Add a Instruction for the new Container-Feature in bwLehrpool.
// TODO: Add link to instructions for Docker-Intetragtion at https://www.bwlehrpool.de/doku.php
/**
* Version for serialization.
*/
private static final long serialVersionUID = 2564301984375080698L;
private final Logger LOGGER = LogManager.getLogger(ContainerUploadPage.class);
private final UploadWizardState state;
private final ImageDetailsRead existingImage;
private ContainerDefinition containerDefinition;
/**
* Page for uploading an Container Image
*
* @param wizard The wizard dialog in which this page is active.
*/
public ContainerUploadPage(Wizard wizard, final UploadWizardState state) {
super(wizard);
this.containerDefinition = new ContainerDefinition();
this.state = state;
canComeBack = false;
existingImage = null;
// HACK set dummy os
state.selectedOs = MetaDataCache.getOsById(18);
init();
}
// This constructor is used in case the user wants do upload a new version.
public ContainerUploadPage(Wizard wizard, UploadWizardState uploadWizardState,
ImageDetailsRead imageDetailsRead) {
super(wizard);
state = uploadWizardState;
existingImage = imageDetailsRead;
containerDefinition = ContainerUtils.getContainerDefinition(
Session.getSatelliteToken(),
imageDetailsRead.getImageName(),
imageDetailsRead.getLatestVersionId());
lblImageName.setEnabled(existingImage == null);
txtImageName.setEnabled(existingImage == null);
txtImageName.setText(existingImage.getImageName());
txtInfoText.setVisible(existingImage == null);
state.name = imageDetailsRead.imageName;
state.defaultPermissions = imageDetailsRead.getDefaultPermissions();
state.description = imageDetailsRead.getDescription();
state.detectedOs = MetaDataCache.getOsById(imageDetailsRead.getOsId());
state.selectedOs = MetaDataCache.getOsById(imageDetailsRead.getOsId());
state.tags = imageDetailsRead.getTags();
state.defaultPermissions = imageDetailsRead.getUserPermissions();
state.uuid = imageDetailsRead.getImageBaseId();
init();
}
public ContainerUploadPage(Wizard wizard, UploadWizardState state,
ContainerDefinition containerDefinition) {
this(wizard, state);
this.containerDefinition = containerDefinition;
}
/**
* register for each user input control the proper action/method
* TODO ugly ...
*/
private void init() {
this.txtImageFile.addActionListener(new ActionListener() {
@Override public void actionPerformed(ActionEvent e) {
browseFile();
}
});
this.btnBrowseForImage.addActionListener(new ActionListener() {
@Override public void actionPerformed(ActionEvent e) {
browseFile();
}
});
txtImageName.getDocument().addDocumentListener(new TextChangeListener() {
@Override public void changed() {
reactOnUserInput();
}
});
tpInput.addChangeListener(new ChangeListener() {
@Override public void stateChanged(ChangeEvent e) {
reactOnUserInput();
}
});
txtGitRepo.getDocument().addDocumentListener(new TextChangeListener() {
@Override public void changed() {
reactOnUserInput();
}
});
txtImageRepo.getDocument().addDocumentListener(new TextChangeListener() {
@Override public void changed() {
reactOnUserInput();
}
});
this.btnBrowseImageTar.addActionListener(new ActionListener() {
@Override public void actionPerformed(ActionEvent e) {
browseImageTarFile();
}
});
this.txtImageTar.addActionListener(new ActionListener() {
@Override public void actionPerformed(ActionEvent e) {
browseImageTarFile();
}
});
btnBrowseForImage.requestFocus();
}
private void browseImageTarFile() {
QFileChooser fc = new QFileChooser(Config.getUploadPath(), false);
fc.setAcceptAllFileFilterUsed(false);
fc.addChoosableFileFilter(new ContainerImageFileFiler());
int action = fc.showOpenDialog(getDialog());
File file = fc.getSelectedFile();
if (action != JFileChooser.APPROVE_OPTION || file == null || !ContainerUtils.isValidTar(file)) {
txtImageTar.setText("");
return;
}
txtImageTar.setText(file.getAbsolutePath());
Config.setUploadPath(file.getParent());
reactOnUserInput();
LOGGER.info("Tar File selected");
}
private void browseFile() {
QFileChooser fc = new QFileChooser(Config.getUploadPath(), false);
fc.setAcceptAllFileFilterUsed(false);
fc.addChoosableFileFilter(new DockerfileFilter());
int action = fc.showOpenDialog(getDialog());
File file = fc.getSelectedFile();
if (action != JFileChooser.APPROVE_OPTION || file == null)
return;
txtImageFile.setText(file.getAbsolutePath());
if (existingImage == null)
txtImageName.setText(file.getParentFile().getName());
else
txtImageName.setText(existingImage.getImageName());
state.descriptionFile = file;
Config.setUploadPath(file.getParent());
reactOnUserInput();
}
private File getDummyFile() {
String configDir = Config.getPath();
File zeroFile = new File(configDir, "ZERO-FILE");
// create a temp file with dummy content,
// because vmchooser relies on an existing image after choosing a lecture.
// TODO change behavior in vmchooser to allow start lectures without images.
try {
if (!zeroFile.exists()) {
zeroFile.createNewFile();
FileUtils.writeStringToFile(zeroFile, "ZERO\n", "UTF-8");
}
} catch (IOException e) {
e.printStackTrace();
LOGGER.error("Could not create a dummy file.", e);
}
LOGGER.info("Use dummy file");
return zeroFile;
}
private void reactOnUserInput() {
boolean completed = checkUserInput();
if (completed)
setDescription(I18n.PAGE.getString("ContainerUploadPage.Description.ContainerDefFinished"));
setPageComplete(completed);
}
private boolean checkUserInput() {
switch (getCurrentContext()) {
case IMAGE_REPOSITORY:
if (txtImageRepo.getText() == null || txtImageRepo.getText().isEmpty()) {
setWarningMessage(I18n.PAGE.getString("ContainerUploadPage.Warning.NoImageRepo"));
return false;
}
break;
case DOCKERFILE:
if (txtImageFile.getText() == null || txtImageFile.getText().isEmpty()) {
setWarningMessage(I18n.PAGE.getString("ContainerUploadPage.Warning.NoReceipt"));
return false;
}
if (!ContainerDefinition.isValidDockerfile(txtImageFile.getText())) {
setWarningMessage(I18n.PAGE.getString("ContainerUploadPage.Warning.NoValidDockerfiler"));
return false;
}
break;
case GIT_REPOSITORY:
if (txtGitRepo.getText() == null || txtGitRepo.getText().isEmpty()) {
setWarningMessage(I18n.PAGE.getString("ContainerUploadPage.Warning.NoGitRepository"));
return false;
}
break;
case DOCKER_ARCHIVE:
if (txtImageTar.getText() == null || txtImageTar.getText().isEmpty()) {
setWarningMessage(I18n.PAGE.getString("ContainerUploadPage.Warning.NoDockerArchive"));
return false;
}
break;
default:
// The case is not provided
return false;
}
if (txtImageName.getText() == null || txtImageName.getText().isEmpty()) {
setWarningMessage(I18n.PAGE.getString("ContainerUploadPage.Warning.NoProperName"));
return false;
}
return true;
}
private VirtualizationConfigurationDocker createVirtualizationConfig() {
containerDefinition.getContainerMeta().setContainerImageContext(getCurrentContext().ordinal());
containerDefinition.getContainerMeta().setImageName(txtImageName.getText());
switch (containerDefinition.getContainerImageContext()) {
case DOCKERFILE:
containerDefinition.setContainerRecipe(state.descriptionFile);
state.diskFile = getDummyFile();
break;
case IMAGE_REPOSITORY:
containerDefinition.getContainerMeta().setImageRepo(txtImageRepo.getText());
state.diskFile = getDummyFile();
state.descriptionFile = getDummyFile();
break;
case GIT_REPOSITORY:
containerDefinition.getContainerMeta().setBuildContextUrl(txtGitRepo.getText());
state.diskFile = getDummyFile();
state.descriptionFile = getDummyFile();
break;
case DOCKER_ARCHIVE:
state.diskFile = new File(txtImageTar.getText());
state.descriptionFile = getDummyFile();
break;
}
return containerDefinition.createVirtualizationConfig();
}
/**
* This function starts the image creation process. It is triggered by the
* "Next" button.
* <p>
* Depending on the state, it will first try to get a UUID for the new image by
* calling createImage() of the thrift API. If a UUID is received, it will
* request an upload with requestImageVersionUpload(). If granted, it will
* finally start a thread for the UploadTask.
* <p>
* Then a callback to the Gui is executed where we can process the upload state
* and give the user feedback about it.
*/
@Override protected boolean wantNextOrFinish() {
state.virtualizationConfig = createVirtualizationConfig();
// -- create image to get uuid --
if (existingImage == null) {
if (state.uuid == null) {
state.name = txtImageName.getText();
state.uuid = ThriftActions.createImage(JOptionPane.getFrameForComponent(this), state.name);
if (state.uuid == null)
return false;
txtImageName.setEnabled(false);
btnBrowseForImage.setEnabled(false);
txtImageFile.setEnabled(false);
}
} else {
state.uuid = existingImage.getImageBaseId();
state.name = existingImage.getImageName();
}
// TODO copy/paste from ImageCreationWizard.wantFinish()
// Do this only if the user wants to upload a new image version!
if (existingImage != null) {
// Create upload initiator that will manage requesting a token, hashing the
// file, connecting for upload...
if (state.upload == null) {
try {
state.upload = new UploadInitiator(state.uuid, state.diskFile,
ByteBuffer.wrap(state.virtualizationConfig.getConfigurationAsByteArray()));
} catch (WrappedException e) {
ThriftError.showMessage(this, LOGGER, e.exception, e.displayMessage);
return false;
} catch (IOException e) {
Gui.showMessageBox(this,
I18n.PAGE.getString("ImageUpload.Message.error.uploadInitiatorFailed"),
MessageType.ERROR, LOGGER, e);
return false;
}
}
// Start the hash check now
state.upload.startHashing();
}
return true;
}
private static class DockerfileFilter extends FileFilter {
@Override public boolean accept(File f) {
Pattern p = Pattern.compile("[Dd]ockerfile");
Matcher m = p.matcher(f.getName());
return (f.isFile() && m.matches()) || f.isDirectory();
}
@Override public String getDescription() {
return "Dockerfile";
}
}
private static class ContainerImageFileFiler extends FileFilter {
@Override public boolean accept(File f) {
return (f.isFile() && f.toString().endsWith(".tar")) || f.isDirectory();
}
@Override public String getDescription() {
return "Container Image (.tar)";
}
}
}