Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.vfs2.FileObject;
import org.apache.hop.core.variables.IVariables;
Expand All @@ -34,6 +35,9 @@ public final class HopInstallSpecFiles {
public static final List<String> WELL_KNOWN_NAMES =
List.of("hop-env.yaml", "hop-env.yml", "hop-env.json", FULL_CLIENT_FILENAME);

/** A VFS scheme: letter, then letters/digits/+/-/. up to a colon. */
private static final Pattern SCHEME = Pattern.compile("^[A-Za-z][A-Za-z0-9+.-]*:");

private HopInstallSpecFiles() {}

public static String resolve(String filename, IVariables variables) {
Expand All @@ -44,6 +48,58 @@ public static String resolve(String filename, IVariables variables) {
return variables != null ? variables.resolve(trimmed) : trimmed;
}

/**
* Resolve a spec file reference for lookup, anchoring a relative reference at the project home.
*
* <p>{@link #resolve(String, IVariables)} only expands variables. A reference that is still
* relative afterwards is handed to VFS, which anchors it at {@code user.dir} — the Hop
* installation directory for a launched Hop GUI — so {@code config/hop-env.yaml} configured on an
* environment points into the install instead of the project (issue #8012). Anchor it at the
* project home instead whenever one is known.
*
* @param filename the configured reference, may be null, blank, relative, or contain variables
* @param variables used to expand variables, may be null
* @param projectHome the project home to anchor at; when blank the {@code PROJECT_HOME} variable
* is used
* @return the resolved reference, unchanged when it is absolute or no project home is known
*/
public static String resolveInProject(String filename, IVariables variables, String projectHome) {
String resolved = resolve(filename, variables);
if (StringUtils.isBlank(resolved) || !isRelative(resolved)) {
return resolved;
}
String home =
StringUtils.isNotBlank(projectHome)
? resolve(projectHome, variables)
: (variables != null
? resolve(variables.getVariable("PROJECT_HOME"), variables)
: null);
if (StringUtils.isBlank(home) || isRelative(home)) {
return resolved;
}
String separator = home.endsWith("/") || home.endsWith("\\") ? "" : "/";
return home + separator + resolved;
}

/**
* Whether a reference still needs a base to be meaningful: no VFS scheme, no leading separator
* and no Windows drive letter.
*/
static boolean isRelative(String filename) {
if (StringUtils.isBlank(filename)) {
return false;
}
String name = filename.trim();
if (name.startsWith("/") || name.startsWith("\\")) {
return false;
}
// A single leading letter followed by a colon is a Windows drive, not a scheme.
if (name.length() >= 2 && Character.isLetter(name.charAt(0)) && name.charAt(1) == ':') {
return false;
}
return !SCHEME.matcher(name).find();
}

public static String baseName(String filename) {
if (StringUtils.isBlank(filename)) {
return "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
package org.apache.hop.marketplace.xp;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.vfs2.FileObject;
import org.apache.hop.core.AttributesContext;
import org.apache.hop.core.Const;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.extension.ExtensionPoint;
import org.apache.hop.core.extension.IExtensionPoint;
import org.apache.hop.core.logging.ILogChannel;
import org.apache.hop.core.variables.IVariables;
import org.apache.hop.core.vfs.HopVfs;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.marketplace.env.HopInstallSpecFiles;
import org.apache.hop.marketplace.env.MarketplaceAttributes;
Expand Down Expand Up @@ -116,10 +118,7 @@ public void callExtensionPoint(
wEditEnv.addListener(
SWT.Selection,
e -> {
String initial = StringUtils.trimToNull(wEnvFile.getText());
if (initial != null && !HopInstallSpecFiles.exists(initial, variables)) {
initial = null;
}
String initial = existingSpecFile(extension, variables);
HopInstallSpecEditor editor = new HopInstallSpecEditor(extension.getShell(), initial);
editor.open();
if (editor.wasSaved() && StringUtils.isNotBlank(editor.getCurrentFilename())) {
Expand All @@ -142,6 +141,7 @@ public void callExtensionPoint(
extension.getShell(),
null,
variables,
browseStart(extension, variables),
new String[] {"*.yaml;*.yml;*.json", "*.*"},
new String[] {
BaseMessages.getString(PKG, "MarketplaceDialog.EnvFile.Filter.Env"),
Expand Down Expand Up @@ -207,6 +207,55 @@ public void callExtensionPoint(
extension.addSaveCallback(this::saveToContext);
}

/**
* The spec file this environment points at, as a reference that actually resolves to a file.
*
* <p>The reference the user typed wins when it already resolves, so a {@code ${PROJECT_HOME}}
* style reference survives a round trip through the editor. Only when it does not resolve is it
* anchored at the project home — otherwise VFS anchors it at the Hop install directory (issue
* #8012).
*
* @return a resolving reference, or null when nothing is configured or it points at no file
*/
private String existingSpecFile(AttributesDialogExtension extension, IVariables variables) {
String configured = StringUtils.trimToNull(wEnvFile.getText());
if (configured == null) {
return null;
}
if (HopInstallSpecFiles.exists(configured, variables)) {
return configured;
}
String anchored =
StringUtils.trimToNull(
HopInstallSpecFiles.resolveInProject(configured, variables, projectHome(extension)));
return anchored != null && HopInstallSpecFiles.exists(anchored, variables) ? anchored : null;
}

/**
* Where the file chooser should start: at the configured spec file, or at the project home when
* nothing usable is configured yet. Returning null leaves the dialog to its own default, which is
* the Hop install directory — the behaviour issue #8012 reports.
*/
private FileObject browseStart(AttributesDialogExtension extension, IVariables variables) {
try {
String existing = existingSpecFile(extension, variables);
if (existing != null) {
return HopVfs.getFileObject(HopInstallSpecFiles.resolve(existing, variables), variables);
}
String home = StringUtils.trimToNull(projectHome(extension));
if (home != null) {
return HopVfs.getFileObject(HopInstallSpecFiles.resolve(home, variables), variables);
}
} catch (Exception e) {
// No usable starting point: let the file dialog choose.
}
return null;
}

private static String projectHome(AttributesDialogExtension extension) {
return extension.getContext() != null ? extension.getContext().getProjectHome() : null;
}

private void loadFromContext(AttributesContext context) {
if (wEnvFile == null || wEnvFile.isDisposed()) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,11 @@ private static Path resolveEnvFile(
AttributesContext context, IVariables variables, Path hopHome) {
String explicit = MarketplaceAttributes.envFile(context);
if (StringUtils.isNotBlank(explicit)) {
Path found = existingSpecPath(explicit.trim(), variables);
// A relative reference belongs to the project, not to the Hop install (issue #8012).
Path found =
existingSpecPath(
HopInstallSpecFiles.resolveInProject(explicit, variables, context.getProjectHome()),
variables);
if (found != null) {
return found;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* 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.apache.hop.marketplace.env;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.hop.core.variables.IVariables;
import org.apache.hop.core.variables.Variables;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

/**
* A relative env file reference configured on a lifecycle environment must be anchored at the
* project home. Anchoring it at {@code user.dir} points it into the Hop installation directory
* (issue #8012).
*/
class HopInstallSpecFilesResolveInProjectTest {

private static IVariables vars() {
IVariables variables = new Variables();
variables.initializeFrom(null);
return variables;
}

private static IVariables variablesWithProjectHome(String home) {
IVariables variables = vars();
if (home != null) {
variables.setVariable("PROJECT_HOME", home);
}
return variables;
}

@Test
void relativeReferenceIsAnchoredAtTheProjectHome() {
assertEquals(
"/home/me/projects/sales/config/hop-env.yaml",
HopInstallSpecFiles.resolveInProject(
"config/hop-env.yaml", vars(), "/home/me/projects/sales"));
}

@Test
void bareFilenameIsAnchoredToo() {
assertEquals(
"/home/me/projects/sales/hop-env.yaml",
HopInstallSpecFiles.resolveInProject("hop-env.yaml", vars(), "/home/me/projects/sales"));
}

@Test
void projectHomeArgumentMayItselfBeAVariable() {
IVariables variables = variablesWithProjectHome("/home/me/projects/sales");
assertEquals(
"/home/me/projects/sales/config/hop-env.yaml",
HopInstallSpecFiles.resolveInProject("config/hop-env.yaml", variables, "${PROJECT_HOME}"));
}

@Test
void projectHomeVariableIsUsedWhenNoHomeIsPassed() {
IVariables variables = variablesWithProjectHome("/home/me/projects/sales");
assertEquals(
"/home/me/projects/sales/config/hop-env.yaml",
HopInstallSpecFiles.resolveInProject("config/hop-env.yaml", variables, null));
}

@Test
void referenceUsingProjectHomeExplicitlyIsResolvedAndLeftAlone() {
IVariables variables = variablesWithProjectHome("/home/me/projects/sales");
assertEquals(
"/home/me/projects/sales/hop-env.yaml",
HopInstallSpecFiles.resolveInProject(
"${PROJECT_HOME}/hop-env.yaml", variables, "/home/me/projects/sales"));
}

@Test
void trailingSeparatorOnTheProjectHomeIsNotDoubled() {
assertEquals(
"/home/me/projects/sales/hop-env.yaml",
HopInstallSpecFiles.resolveInProject("hop-env.yaml", vars(), "/home/me/projects/sales/"));
}

@Test
void absoluteReferencesAreNeverAnchored() {
assertEquals(
"/etc/hop/hop-env.yaml",
HopInstallSpecFiles.resolveInProject(
"/etc/hop/hop-env.yaml", vars(), "/home/me/projects/sales"));
assertEquals(
"C:\\hop\\hop-env.yaml",
HopInstallSpecFiles.resolveInProject(
"C:\\hop\\hop-env.yaml", vars(), "/home/me/projects/sales"));
assertEquals(
"\\\\server\\share\\hop-env.yaml",
HopInstallSpecFiles.resolveInProject(
"\\\\server\\share\\hop-env.yaml", vars(), "/home/me/projects/sales"));
}

@Test
void vfsReferencesAreNeverAnchored() {
assertEquals(
"s3://bucket/hop-env.yaml",
HopInstallSpecFiles.resolveInProject(
"s3://bucket/hop-env.yaml", vars(), "/home/me/projects/sales"));
assertEquals(
"file:///etc/hop/hop-env.yaml",
HopInstallSpecFiles.resolveInProject(
"file:///etc/hop/hop-env.yaml", vars(), "/home/me/projects/sales"));
}

@Test
void withoutAProjectHomeTheReferenceIsHandedOnUnchanged() {
// No project home anywhere: keep the previous behaviour rather than invent a base.
assertEquals(
"config/hop-env.yaml",
HopInstallSpecFiles.resolveInProject("config/hop-env.yaml", vars(), null));
assertEquals(
"config/hop-env.yaml",
HopInstallSpecFiles.resolveInProject("config/hop-env.yaml", vars(), " "));
}

@Test
void aRelativeProjectHomeIsNoBetterThanNone() {
assertEquals(
"hop-env.yaml",
HopInstallSpecFiles.resolveInProject("hop-env.yaml", vars(), "projects/sales"));
}

@Test
void blankReferencesStayBlank() {
assertNull(HopInstallSpecFiles.resolveInProject(null, vars(), "/home/me"));
assertEquals("", HopInstallSpecFiles.resolveInProject(" ", vars(), "/home/me").trim());
}

@Test
void nullVariablesAreTolerated() {
assertEquals(
"/home/me/hop-env.yaml",
HopInstallSpecFiles.resolveInProject("hop-env.yaml", null, "/home/me"));
}

/**
* The defect itself: a project relative reference handed straight to VFS is looked up under
* {@code user.dir} — the Hop install for a launched Hop GUI — so the file the user configured is
* not found, while the same reference anchored at the project home is.
*/
@Test
void relativeReferenceIsOnlyFoundOnceAnchoredAtTheProject(@TempDir Path projectHome)
throws Exception {
Path config = Files.createDirectories(projectHome.resolve("config"));
Files.writeString(
config.resolve("hop-env.yaml"),
"version: \"1.0\"\nhopVersion: \"2.19.0\"\n",
StandardCharsets.UTF_8);
IVariables variables = vars();

assertFalse(
HopInstallSpecFiles.exists(
HopInstallSpecFiles.resolve("config/hop-env.yaml", variables), variables),
"a relative reference resolves against user.dir, not the project");
assertTrue(
HopInstallSpecFiles.exists(
HopInstallSpecFiles.resolveInProject(
"config/hop-env.yaml", variables, projectHome.toString()),
variables));
}

@Test
void isRelativeRecognisesSchemesDrivesAndRoots() {
assertTrue(HopInstallSpecFiles.isRelative("hop-env.yaml"));
assertTrue(HopInstallSpecFiles.isRelative("config/hop-env.yaml"));
assertTrue(HopInstallSpecFiles.isRelative("../hop-env.yaml"));
assertFalse(HopInstallSpecFiles.isRelative("/hop-env.yaml"));
assertFalse(HopInstallSpecFiles.isRelative("D:/hop/hop-env.yaml"));
assertFalse(HopInstallSpecFiles.isRelative("hdfs://nn:8020/hop-env.yaml"));
assertFalse(HopInstallSpecFiles.isRelative(null));
}
}
Loading
Loading