diff --git a/apps/openadt-bootstrap/project.json b/apps/openadt-bootstrap/project.json index b5ea995a..9e2e495c 100644 --- a/apps/openadt-bootstrap/project.json +++ b/apps/openadt-bootstrap/project.json @@ -11,7 +11,7 @@ "executor": "nx:run-commands", "dependsOn": ["openadt-config:compile", "openadt-sap-adt:compile"], "options": { - "command": "node scripts/mvnw-runner.mjs -q compile -pl apps/openadt-bootstrap -am -f pom.xml", + "command": "node scripts/mvnw-runner.mjs -q install -pl apps/openadt-bootstrap -Pdistribution -Dopenadt.distribution=true -DskipTests -f pom.xml", "cwd": "{workspaceRoot}" }, "cache": true, @@ -22,7 +22,7 @@ "executor": "nx:run-commands", "dependsOn": ["compile"], "options": { - "command": "node scripts/mvnw-runner.mjs -q test -pl apps/openadt-bootstrap -am -f pom.xml", + "command": "node scripts/mvnw-runner.mjs -q test -pl apps/openadt-bootstrap -Pdistribution -Dopenadt.distribution=true -f pom.xml && node scripts/mvnw-runner.mjs install -pl apps/openadt-bootstrap -Pdistribution -Dopenadt.distribution=true -DskipTests -f pom.xml", "cwd": "{workspaceRoot}" }, "cache": true, @@ -37,7 +37,7 @@ "openadt-sap-adt:package" ], "options": { - "command": "node scripts/mvnw-runner.mjs -q package -pl apps/openadt-bootstrap -am -DskipTests -f pom.xml", + "command": "node scripts/mvnw-runner.mjs -q package -pl apps/openadt-bootstrap -Pdistribution -Dopenadt.distribution=true -DskipTests -f pom.xml", "cwd": "{workspaceRoot}" }, "cache": true, diff --git a/apps/openadt-bootstrap/src/main/java/org/openadt/bootstrap/SapGuiLandscapeDetector.java b/apps/openadt-bootstrap/src/main/java/org/openadt/bootstrap/SapGuiLandscapeDetector.java index 03018f7f..d3d8371a 100644 --- a/apps/openadt-bootstrap/src/main/java/org/openadt/bootstrap/SapGuiLandscapeDetector.java +++ b/apps/openadt-bootstrap/src/main/java/org/openadt/bootstrap/SapGuiLandscapeDetector.java @@ -52,12 +52,26 @@ private List parseLandscapeFile(Path path) throws IOException { List systems = new ArrayList<>(); parseClassicSystems(document, systems); parseLoadBalancedSystems(document, systems); + detectExternalLandscapeUrls(document, path); return systems; } catch (ParserConfigurationException | SAXException e) { throw new IOException("Failed to parse SAP GUI landscape: " + path, e); } } + private void detectExternalLandscapeUrls(Document document, Path sourcePath) { + NodeList includeNodes = document.getElementsByTagName("Include"); + for (int i = 0; i < includeNodes.getLength(); i++) { + Element include = (Element) includeNodes.item(i); + String url = blankToNull(include.getAttribute("url")); + if (url != null) { + System.err.println("INFO: External SAP landscape URL detected: " + url); + System.err.println("INFO: Some system details (message server hostnames) may be in the external landscape."); + System.err.println("INFO: Open SAP GUI once to cache the full landscape, or provide message server manually."); + } + } + } + private void parseClassicSystems(Document document, List systems) { NodeList systemNodes = document.getElementsByTagName("System"); for (int i = 0; i < systemNodes.getLength(); i++) { diff --git a/apps/openadt-bootstrap/src/main/java/org/openadt/bootstrap/SetupPathLocator.java b/apps/openadt-bootstrap/src/main/java/org/openadt/bootstrap/SetupPathLocator.java index 36b34bcd..6db650d7 100644 --- a/apps/openadt-bootstrap/src/main/java/org/openadt/bootstrap/SetupPathLocator.java +++ b/apps/openadt-bootstrap/src/main/java/org/openadt/bootstrap/SetupPathLocator.java @@ -36,6 +36,18 @@ static List sapGuiLandscapeFiles() { } } else if (os.contains("mac")) { paths.add(Path.of(home, "Library", "Application Support", "SAP", SAP_COMMON, SAPUI_LANDSCAPE)); + // SAP GUI for Java stores landscape in Preferences with different filename + paths.add(Path.of(home, "Library", "Preferences", "SAP", "SAPGUILandscape.xml")); + // SAP GUI for Java caches full landscape with message servers + Path cacheDir = Path.of(home, "Library", "Preferences", "SAP", "Cache", "SAPUILandscape"); + if (Files.exists(cacheDir)) { + try (var stream = Files.list(cacheDir)) { + stream.filter(f -> f.toString().endsWith(".xml")) + .forEach(paths::add); + } catch (IOException e) { + // Ignore cache read errors + } + } } for (Path windowsHome : windowsUserHomes()) { @@ -103,6 +115,23 @@ static List eclipseWorkspacePaths() { static List jcoJarRoots() { List paths = new ArrayList<>(); + String os = System.getProperty(OS_NAME_PROPERTY, "").toLowerCase(Locale.ROOT); + String home = System.getProperty(USER_HOME_PROPERTY, ""); + + // macOS and Linux: check user's home directory for Eclipse p2 pool + if (!home.isBlank() && (os.contains("mac") || os.contains("nix") || os.contains("nux"))) { + paths.add(Path.of(home, ".p2", "pool", "plugins")); + } + + // macOS: Eclipse.app bundle installations + if (os.contains("mac")) { + paths.add(Path.of("/Applications", "Eclipse.app", "Contents", "Eclipse", "plugins")); + paths.add(Path.of("/Applications", "SAP Business Application Studio.app", "Contents", "Eclipse", "plugins")); + if (!home.isBlank()) { + paths.add(Path.of(home, "Applications", "Eclipse.app", "Contents", "Eclipse", "plugins")); + } + } + for (Path windowsHome : windowsUserHomes()) { paths.add(windowsHome.resolve(".p2/pool/plugins")); } @@ -113,6 +142,20 @@ static List jcoJarRoots() { static List jcoNativeSearchRoots() { List paths = new ArrayList<>(); + String os = System.getProperty(OS_NAME_PROPERTY, "").toLowerCase(Locale.ROOT); + String home = System.getProperty(USER_HOME_PROPERTY, ""); + + // macOS-specific paths for JCo native libraries + if (os.contains("mac") && !home.isBlank()) { + paths.add(Path.of(home, "lib")); + paths.add(Path.of(home, ".p2")); + paths.add(Path.of("/usr", "local", "lib")); + paths.add(Path.of("/Library", "Java", "Extensions")); + // Eclipse.app OSGi bundle cache (where JCo native lib is extracted) + paths.add(Path.of("/Applications", "Eclipse.app", "Contents", "Eclipse", "configuration", "org.eclipse.osgi")); + paths.add(Path.of(home, "Applications", "Eclipse.app", "Contents", "Eclipse", "configuration", "org.eclipse.osgi")); + } + for (Path windowsHome : windowsUserHomes()) { paths.add(windowsHome.resolve("Documents")); paths.add(windowsHome.resolve("AppData/Local")); @@ -144,6 +187,16 @@ private static Path openadtSdkRoot() { static List sapcryptoCandidates() { List paths = new ArrayList<>(); + String os = System.getProperty(OS_NAME_PROPERTY, "").toLowerCase(Locale.ROOT); + String home = System.getProperty(USER_HOME_PROPERTY, ""); + + // macOS-specific paths for SAP Crypto + if (os.contains("mac") && !home.isBlank()) { + paths.add(Path.of(home, "lib", "libsapcrypto.dylib")); + paths.add(Path.of("/usr", "local", "lib", "libsapcrypto.dylib")); + paths.add(Path.of("/Library", "Frameworks", "sapcrypto.framework", "Versions", "Current", "libsapcrypto.dylib")); + } + for (Path programFilesRoot : windowsProgramFilesRoots()) { paths.add(programFilesRoot.resolve("SAP/FrontEnd/SecureLogin/lib/sapcrypto.dll")); } diff --git a/apps/openadt-cli/project.json b/apps/openadt-cli/project.json index 363e42ef..ec78f4df 100644 --- a/apps/openadt-cli/project.json +++ b/apps/openadt-cli/project.json @@ -15,7 +15,7 @@ "openadt-bootstrap:compile" ], "options": { - "command": "node scripts/mvnw-runner.mjs -q compile -pl apps/openadt-cli -am -Pdistribution -Dopenadt.distribution=true -f pom.xml && bun run scripts/dev-runtime-classpath.ts", + "command": "node scripts/mvnw-runner.mjs -q install -pl apps/openadt-cli -Pdistribution -Dopenadt.distribution=true -DskipTests -f pom.xml && bun run scripts/dev-runtime-classpath.ts", "cwd": "{workspaceRoot}" }, "cache": true, @@ -26,7 +26,7 @@ "executor": "nx:run-commands", "dependsOn": ["package"], "options": { - "command": "node scripts/mvnw-runner.mjs -q test -pl apps/openadt-cli -am -Pdistribution -Dopenadt.distribution=true -f pom.xml", + "command": "node scripts/mvnw-runner.mjs -q test -pl apps/openadt-cli -Pdistribution -Dopenadt.distribution=true -f pom.xml", "cwd": "{workspaceRoot}" }, "cache": true, @@ -36,7 +36,7 @@ "package": { "executor": "nx:run-commands", "options": { - "command": "node scripts/mvnw-runner.mjs -q package -pl apps/openadt-cli -am -Pdistribution -Dopenadt.distribution=true -DskipTests -f pom.xml", + "command": "node scripts/mvnw-runner.mjs -q package -pl apps/openadt-cli -Pdistribution -Dopenadt.distribution=true -DskipTests -f pom.xml", "cwd": "{workspaceRoot}" }, "cache": true, diff --git a/apps/openadt-config/project.json b/apps/openadt-config/project.json index 547bb90b..e9ac5bb3 100644 --- a/apps/openadt-config/project.json +++ b/apps/openadt-config/project.json @@ -10,7 +10,7 @@ "compile": { "executor": "nx:run-commands", "options": { - "command": "node scripts/mvnw-runner.mjs -q compile -pl apps/openadt-config -f pom.xml", + "command": "node scripts/mvnw-runner.mjs -q install -N -Pdistribution -Dopenadt.distribution=true -DskipTests -f pom.xml && node scripts/mvnw-runner.mjs -q install -pl apps/openadt-config -Pdistribution -Dopenadt.distribution=true -DskipTests -f pom.xml", "cwd": "{workspaceRoot}" }, "cache": true, @@ -21,7 +21,7 @@ "executor": "nx:run-commands", "dependsOn": ["compile"], "options": { - "command": "node scripts/mvnw-runner.mjs -q test -pl apps/openadt-config -f pom.xml", + "command": "node scripts/mvnw-runner.mjs -q test -pl apps/openadt-config -Pdistribution -Dopenadt.distribution=true -f pom.xml && node scripts/mvnw-runner.mjs -q install -pl apps/openadt-config -Pdistribution -Dopenadt.distribution=true -DskipTests -f pom.xml", "cwd": "{workspaceRoot}" }, "cache": true, @@ -32,7 +32,7 @@ "executor": "nx:run-commands", "dependsOn": ["compile"], "options": { - "command": "node scripts/mvnw-runner.mjs -q package -pl apps/openadt-config -DskipTests -f pom.xml", + "command": "node scripts/mvnw-runner.mjs -q package -pl apps/openadt-config -Pdistribution -Dopenadt.distribution=true -DskipTests -f pom.xml", "cwd": "{workspaceRoot}" }, "cache": true, diff --git a/apps/openadt-sap-adt/project.json b/apps/openadt-sap-adt/project.json index 1ebc3d2b..cdb8989f 100644 --- a/apps/openadt-sap-adt/project.json +++ b/apps/openadt-sap-adt/project.json @@ -11,7 +11,7 @@ "executor": "nx:run-commands", "dependsOn": ["openadt-config:compile"], "options": { - "command": "node scripts/mvnw-runner.mjs -q compile -pl apps/openadt-sap-adt -am -f pom.xml", + "command": "node scripts/mvnw-runner.mjs -q install -pl apps/openadt-sap-adt -Pdistribution -Dopenadt.distribution=true -DskipTests -f pom.xml", "cwd": "{workspaceRoot}" }, "cache": true, @@ -22,7 +22,7 @@ "executor": "nx:run-commands", "dependsOn": ["compile"], "options": { - "command": "node scripts/mvnw-runner.mjs -q test -pl apps/openadt-sap-adt -am -f pom.xml", + "command": "node scripts/mvnw-runner.mjs -q test -pl apps/openadt-sap-adt -Pdistribution -Dopenadt.distribution=true -f pom.xml && node scripts/mvnw-runner.mjs install -pl apps/openadt-sap-adt -Pdistribution -Dopenadt.distribution=true -DskipTests -f pom.xml", "cwd": "{workspaceRoot}" }, "cache": true, @@ -33,7 +33,7 @@ "executor": "nx:run-commands", "dependsOn": ["compile", "openadt-config:package"], "options": { - "command": "node scripts/mvnw-runner.mjs -q package -pl apps/openadt-sap-adt -am -DskipTests -f pom.xml", + "command": "node scripts/mvnw-runner.mjs -q package -pl apps/openadt-sap-adt -Pdistribution -Dopenadt.distribution=true -DskipTests -f pom.xml", "cwd": "{workspaceRoot}" }, "cache": true, diff --git a/apps/openadt-sap-adt/src/main/java/org/openadt/sap/adt/bootstrap/SecureLoginHubClient.java b/apps/openadt-sap-adt/src/main/java/org/openadt/sap/adt/bootstrap/SecureLoginHubClient.java index 0fb1839a..a06fcaf2 100644 --- a/apps/openadt-sap-adt/src/main/java/org/openadt/sap/adt/bootstrap/SecureLoginHubClient.java +++ b/apps/openadt-sap-adt/src/main/java/org/openadt/sap/adt/bootstrap/SecureLoginHubClient.java @@ -88,6 +88,15 @@ public boolean isReachable() { public String webAdapterStatus(String profileId) throws IOException, InterruptedException { String encodedProfileId = URLEncoder.encode(profileId, StandardCharsets.UTF_8); + // Try API v3 first (Windows), fall back to v2 (macOS) + try { + return webAdapterStatusV3(encodedProfileId); + } catch (IOException e) { + return webAdapterStatusV2(encodedProfileId); + } + } + + private String webAdapterStatusV3(String encodedProfileId) throws IOException, InterruptedException { URI uri = URI.create(hubBaseUrl + "/slc3/api/status?profileid=" + encodedProfileId); HttpResponse response = httpClient.send( hubRequest(uri).GET().build(), @@ -101,11 +110,34 @@ public String webAdapterStatus(String profileId) throws IOException, Interrupted return status.isTextual() ? status.asText() : "UNKNOWN"; } + private String webAdapterStatusV2(String encodedProfileId) throws IOException, InterruptedException { + URI uri = URI.create(hubBaseUrl + "/slc2/api/status?profileid=" + encodedProfileId); + HttpResponse response = httpClient.send( + hubRequest(uri).GET().build(), + HttpResponse.BodyHandlers.ofString() + ); + if (response.statusCode() != 200) { + throw new IOException("Secure Login hub v2 status failed with HTTP " + response.statusCode()); + } + JsonNode root = objectMapper.readTree(response.body()); + JsonNode status = root.path("status"); + return status.isTextual() ? status.asText() : "UNKNOWN"; + } + public void loginWebAdapter(String profileId) throws IOException, InterruptedException { loginWebAdapter(profileId, false); } public void loginWebAdapter(String profileId, boolean browserMonitor) throws IOException, InterruptedException { + // Try API v3 first (Windows), fall back to v2 (macOS) + try { + loginWebAdapterV3(profileId, browserMonitor); + } catch (IOException e) { + loginWebAdapterV2(profileId, browserMonitor); + } + } + + private void loginWebAdapterV3(String profileId, boolean browserMonitor) throws IOException, InterruptedException { URI uri = URI.create(hubBaseUrl + "/slc3/api/login"); java.util.Map clientConfig = new java.util.LinkedHashMap<>(); clientConfig.put("browserMonitor", browserMonitor); @@ -130,6 +162,31 @@ public void loginWebAdapter(String profileId, boolean browserMonitor) throws IOE } } + private void loginWebAdapterV2(String profileId, boolean browserMonitor) throws IOException, InterruptedException { + URI uri = URI.create(hubBaseUrl + "/slc2/api/login"); + java.util.Map clientConfig = new java.util.LinkedHashMap<>(); + clientConfig.put("browserMonitor", browserMonitor); + clientConfig.put("inactivityTimeout", 0); + clientConfig.put("keySize", 2048); + clientConfig.put("profileSLWA", profileId); + clientConfig.put("localport", 34443); + clientConfig.put("autoLogOut", false); + java.util.Map payload = new java.util.LinkedHashMap<>(); + payload.put("profileid", profileId); + payload.put("clientConfig", clientConfig); + String body = objectMapper.writeValueAsString(payload); + HttpResponse response = httpClient.send( + hubRequest(uri) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(), + HttpResponse.BodyHandlers.ofString() + ); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new IOException("Secure Login hub v2 login failed with HTTP " + response.statusCode()); + } + } + public void ensureWebAdapterLoggedIn(String profileId) throws IOException, InterruptedException { ensureWebAdapterLoggedIn(profileId, false); } diff --git a/specs/setup.md b/specs/setup.md index 1c9ca067..cf1d9522 100644 --- a/specs/setup.md +++ b/specs/setup.md @@ -42,9 +42,12 @@ Lookup paths: - **Windows**: `%APPDATA%\SAP\Common\SAPUILandscape.xml` - **macOS**: `~/Library/Application Support/SAP/Common/SAPUILandscape.xml` +- **macOS (SAP GUI for Java)**: `~/Library/Preferences/SAP/SAPGUILandscape.xml` - **WSL**: `/mnt/c/Users//AppData/Roaming/SAP/Common/SAPUILandscape.xml` - **WSL**: `/mnt/c/Users//AppData/Roaming/SAP/LogonServerConfigCache/*.xml` +When the landscape file contains `` tags pointing to external landscapes (common with centrally-managed SAP GUI deployments), the detector logs an informational message. Full system details (including message server hostnames) may require opening SAP GUI once to cache the landscape locally, or manual configuration of the `mshost` field. + For classic `` entries, extracts: - `name` → alias, description