blob: 5d3e0c5ac7d43c34ae98d05464a8c4859b9d9915 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.docker.ui.build2;
import java.io.IOException;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.modules.docker.api.DockerAction;
import org.netbeans.modules.docker.api.DockerInstance;
import org.netbeans.modules.docker.api.DockerfileDetail;
import org.openide.WizardDescriptor;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileUtil;
import org.openide.util.ChangeSupport;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
public class BuildOptionsPanel implements WizardDescriptor.Panel<WizardDescriptor>, ChangeListener {
private final ChangeSupport changeSupport = new ChangeSupport(this);
private final RequestProcessor RP = new RequestProcessor("Parsing Dockerfile", 1);
/**
* The visual component that displays this panel. If you need to access the
* component from this class, just use getComponent().
*/
private BuildOptionsVisual component;
private WizardDescriptor wizard;
public BuildOptionsPanel() {
super();
}
// Get the visual component for the panel. In this template, the component
// is kept separate. This can be more efficient: if the wizard is created
// but never displayed, or not all panels are displayed, it is better to
// create only those which really need to be visible.
@Override
public BuildOptionsVisual getComponent() {
if (component == null) {
component = new BuildOptionsVisual();
component.addChangeListener(this);
}
return component;
}
@Override
public HelpCtx getHelp() {
// Show no Help button for this panel:
return HelpCtx.DEFAULT_HELP;
// If you have context help:
// return new HelpCtx("help.key.here");
}
@NbBundle.Messages({
"MSG_NonExistingDockerfile=The Dockerfile does not exist.",
"MSG_NonRelativeDockerfile=The Dockerfile must be inside the build context."
})
@Override
public boolean isValid() {
// clear the error message
wizard.putProperty(WizardDescriptor.PROP_ERROR_MESSAGE, null);
wizard.putProperty(WizardDescriptor.PROP_INFO_MESSAGE, null);
wizard.putProperty(WizardDescriptor.PROP_WARNING_MESSAGE, null);
String buildContext = (String) wizard.getProperty(BuildImageWizard.BUILD_CONTEXT_PROPERTY);
String dockerfile = component.getDockerfile();
if (dockerfile == null) {
dockerfile = buildContext + "/" + DockerAction.DOCKER_FILE;
} else {
dockerfile = buildContext + "/" + dockerfile;
}
FileSystem fs = (FileSystem) wizard.getProperty(BuildImageWizard.FILESYSTEM_PROPERTY);
FileObject fo = fs.getRoot().getFileObject(dockerfile);
// the last check avoids entires like Dockerfile/ to be considered valid files
if (fo == null || !fo.isData() || !dockerfile.endsWith(fo.getNameExt())) {
wizard.putProperty(WizardDescriptor.PROP_ERROR_MESSAGE, Bundle.MSG_NonExistingDockerfile());
return false;
}
FileObject build = fs.getRoot().getFileObject(buildContext);
if (build == null || !FileUtil.isParentOf(build, fo)) {
wizard.putProperty(WizardDescriptor.PROP_ERROR_MESSAGE, Bundle.MSG_NonRelativeDockerfile());
return false;
}
// We start dockerfile parsing only if dockerfile is valid.
// That's why this code is located here, not in readSettings
Map<String, String> buildArguments = (Map<String, String>) wizard.getProperty(BuildImageWizard.BUILD_ARGUMENTS_PROPERTY);
if (buildArguments == null) {
RP.submit(new BuildArgsUpdaterTask(fo));
}
return true;
}
@Override
public void addChangeListener(ChangeListener l) {
changeSupport.addChangeListener(l);
}
@Override
public void removeChangeListener(ChangeListener l) {
changeSupport.removeChangeListener(l);
}
@Override
public void readSettings(WizardDescriptor wiz) {
if (wizard == null) {
wizard = wiz;
}
component.setBuildContext((String) wiz.getProperty(BuildImageWizard.BUILD_CONTEXT_PROPERTY));
String dockerfile = (String) wiz.getProperty(BuildImageWizard.DOCKERFILE_PROPERTY);
if (dockerfile == null) {
dockerfile = DockerAction.DOCKER_FILE;
}
component.setDockerfile(dockerfile);
Boolean pull = (Boolean) wiz.getProperty(BuildImageWizard.PULL_PROPERTY);
component.setPull(pull != null ? pull : BuildImageWizard.PULL_DEFAULT);
Boolean noCache = (Boolean) wiz.getProperty(BuildImageWizard.NO_CACHE_PROPERTY);
component.setPull(noCache != null ? noCache : BuildImageWizard.NO_CACHE_DEFAULT);
// XXX revalidate; is this bug?
changeSupport.fireChange();
}
@Override
public void storeSettings(WizardDescriptor wiz) {
wiz.putProperty(BuildImageWizard.DOCKERFILE_PROPERTY, component.getDockerfile());
wiz.putProperty(BuildImageWizard.PULL_PROPERTY, component.isPull());
wiz.putProperty(BuildImageWizard.NO_CACHE_PROPERTY, component.isNoCache());
wiz.putProperty(BuildImageWizard.BUILD_ARGUMENTS_PROPERTY, component.getBuildArgs());
}
@Override
public void stateChanged(ChangeEvent e) {
changeSupport.fireChange();
}
private static final Pattern MACRO_PATTERN = Pattern.compile("(?<!\\\\)\\$");
private class BuildArgsUpdaterTask implements Runnable {
private final FileObject fo;
private DockerfileDetail dockerfileDetail;
public BuildArgsUpdaterTask(FileObject fo) {
this.fo = fo;
}
@Override
public void run() {
if (SwingUtilities.isEventDispatchThread()) {
Map<String, String> userDefinedBuildArgs = component.getBuildArgs();
// Submit changes iff user haven't start filling arguments table yet.
if (userDefinedBuildArgs == null || userDefinedBuildArgs.isEmpty()) {
Map<String, String> filtered = filterMacros(dockerfileDetail.getBuildArgs());
component.setBuildArgs(filtered);
wizard.putProperty(BuildImageWizard.BUILD_ARGUMENTS_PROPERTY, filtered);
}
} else {
try {
// null is OK
DockerInstance instance = (DockerInstance) wizard.getProperty(BuildImageWizard.INSTANCE_PROPERTY);
dockerfileDetail = new DockerAction(instance).getDetail(fo);
SwingUtilities.invokeLater(this);
} catch (IOException ex) {
Logger.getLogger(BuildOptionsPanel.class.getName()).log(Level.INFO, "Can't parse dockerfile: {0}", ex.toString());
}
}
}
}
/**
* Remove all ARGS that contain macros, i.e. SOMEARG=$ANOTHER. Macros like
* PRICE=\$100 are valid.
*/
private Map<String, String> filterMacros(Map<String, String> map) {
return map.entrySet().stream()
.filter((entry) -> !MACRO_PATTERN.matcher(entry.getValue()).find())
.collect(Collectors.toMap((entry) -> entry.getKey(), (entry) -> entry.getValue()));
}
}