code: // Licensed to the Software Freedom Conservancy (SFC) under one
  // or more contributor license agreements.  See the NOTICE file
  // distributed with this work for additional information
  // regarding copyright ownership.  The SFC 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.openqa.selenium.firefox;
  
  import static java.util.Arrays.stream;
  import static java.util.stream.Collectors.toList;
  import static org.openqa.selenium.Platform.MAC;
  import static org.openqa.selenium.Platform.UNIX;
  import static org.openqa.selenium.Platform.WINDOWS;
  
  import java.io.File;
  import java.io.IOException;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.stream.Collectors;
  import java.util.stream.Stream;
  import org.openqa.selenium.Platform;
  import org.openqa.selenium.WebDriverException;
  import org.openqa.selenium.os.ExecutableFinder;
  
  public class FirefoxBinary {
  
    /** Enumerates Firefox channels, according to https://wiki.mozilla.org/RapidRelease */
    public enum Channel {
      ESR("esr"),
      RELEASE("release"),
      BETA("beta"),
      AURORA("aurora"),
      NIGHTLY("nightly");
  
      private String name;
  
      Channel(String name) {
        this.name = name;
      }
  
      public String toString() {
        return name;
      }
  
      /**
       * Gets a channel with the name matching the parameter.
       *
       * @param name the channel name
       * @return the Channel enum value matching the parameter
       */
      public static Channel fromString(String name) {
        final String lcName = name.toLowerCase();
        return stream(Channel.values())
            .filter(ch -> ch.name.equals(lcName))
            .findFirst()
            .orElseThrow(() -> new WebDriverException("Unrecognized channel: " + name));
      }
    }
  
    private final List<String> extraOptions = new ArrayList<>();
    private final Executable executable;
  
    public FirefoxBinary() {
      Executable systemBinary = locateFirefoxBinaryFromSystemProperty();
      if (systemBinary != null) {
        executable = systemBinary;
        return;
      }
  
      Executable platformBinary = locateFirefoxBinariesFromPlatform().findFirst().orElse(null);
      if (platformBinary != null) {
        executable = platformBinary;
        return;
      }
  
      throw new WebDriverException(
          "Cannot find firefox binary in PATH. "
              + "Make sure firefox is installed. OS appears to be: "
              + Platform.getCurrent());
    }
  
    public FirefoxBinary(Channel channel) {
      Executable systemBinary = locateFirefoxBinaryFromSystemProperty();
      if (systemBinary != null) {
        if (systemBinary.getChannel() == channel) {
          executable = systemBinary;
          return;
        } else {
          throw new WebDriverException(
              "Firefox executable specified by system property "
                  + FirefoxDriver.SystemProperty.BROWSER_BINARY
                  + " does not belong to channel '"
                  + channel
                  + "', it appears to be '"
                  + systemBinary.getChannel()
                  + "'");
        }
      }
  
      executable =
          locateFirefoxBinariesFromPlatform()
              .filter(e -> e.getChannel() == channel)
              .findFirst()
              .orElseThrow(
                  () ->
                      new WebDriverException(
                          String.format(
                              "Cannot find firefox binary for channel '%s' in PATH", channel)));
    }
  
    public FirefoxBinary(File pathToFirefoxBinary) {
      executable = new Executable(pathToFirefoxBinary);
    }
  
    public void addCommandLineOptions(String... options) {
      Collections.addAll(extraOptions, options);
    }
  
    void amendOptions(FirefoxOptions options) {
      options.addArguments(extraOptions);
    }
  
    public File getFile() {
      return executable.getFile();
    }
  
    public String getPath() {
      return executable.getPath();
    }
  
    public List<String> getExtraOptions() {
      return extraOptions;
    }
  
    @Override
    public String toString() {
      return "FirefoxBinary(" + executable.getPath() + ")";
    }
  
    public String toJson() {
      return executable.getPath();
    }
  
    /**
     * Locates the firefox binary from a system property. Will throw an exception if the binary cannot
     * be found.
     */
    static Executable locateFirefoxBinaryFromSystemProperty() {
      String binaryName = System.getProperty(FirefoxDriver.SystemProperty.BROWSER_BINARY);
      if (binaryName == null) return null;
  
      File binary = new File(binaryName);
      if (binary.exists() && !binary.isDirectory()) return new Executable(binary);
  
      Platform current = Platform.getCurrent();
      if (current.is(WINDOWS)) {
        if (!binaryName.endsWith(".exe")) {
          binaryName += ".exe";
        }
  
      } else if (current.is(MAC)) {
        if (!binaryName.endsWith(".app")) {
          binaryName += ".app";
        }
        binaryName += "/Contents/MacOS/firefox-bin";
      }
  
      binary = new File(binaryName);
      if (binary.exists()) return new Executable(binary);
  
      throw new WebDriverException(
          String.format(
              "'%s' property set, but unable to locate the requested binary: %s",
              FirefoxDriver.SystemProperty.BROWSER_BINARY, binaryName));
    }
  
    /** Locates the firefox binary by platform. */
    private static Stream<Executable> locateFirefoxBinariesFromPlatform() {
      List<Executable> executables = new ArrayList<>();
  
      Platform current = Platform.getCurrent();
      if (current.is(WINDOWS)) {
        executables.addAll(
            Stream.of(
                    "Mozilla Firefox\\firefox.exe",
                    "Firefox Developer Edition\\firefox.exe",
                    "Nightly\\firefox.exe")
                .map(FirefoxBinary::getPathsInProgramFiles)
                .flatMap(List::stream)
                .map(File::new)
                .filter(File::exists)
                .map(Executable::new)
                .collect(toList()));
  
      } else if (current.is(MAC)) {
        // system
        File binary = new File("/Applications/Firefox.app/Contents/MacOS/firefox-bin");
        if (binary.exists()) {
          executables.add(new Executable(binary));
        }
  
        // user home
        binary = new File(System.getProperty("user.home") + binary.getAbsolutePath());
        if (binary.exists()) {
          executables.add(new Executable(binary));
        }
  
      } else if (current.is(UNIX)) {
        String systemFirefoxBin = new ExecutableFinder().find("firefox-bin");
        if (systemFirefoxBin != null) {
          executables.add(new Executable(new File(systemFirefoxBin)));
        }
      }
  
      String systemFirefox = new ExecutableFinder().find("firefox");
      if (systemFirefox != null) {
        Path firefoxPath = new File(systemFirefox).toPath();
        if (Files.isSymbolicLink(firefoxPath)) {
          try {
            Path realPath = firefoxPath.toRealPath();
            File attempt1 = realPath.getParent().resolve("firefox").toFile();
            if (attempt1.exists()) {
              executables.add(new Executable(attempt1));
            } else {
              File attempt2 = realPath.getParent().resolve("firefox-bin").toFile();
              if (attempt2.exists()) {
                executables.add(new Executable(attempt2));
              }
            }
          } catch (IOException e) {
            // ignore this path
          }
  
        } else {
          executables.add(new Executable(new File(systemFirefox)));
        }
      }
  
      return executables.stream();
    }
  
    private static List<String> getPathsInProgramFiles(final String childPath) {
      return Stream.of(getProgramFilesPath(), getProgramFiles86Path())
          .map(parent -> new File(parent, childPath).getAbsolutePath())
          .collect(Collectors.toList());
    }
  
    /**
     * Returns the path to the Windows Program Files. On non-English versions, this is not necessarily
     * "C:\Program Files".
     *
     * @return the path to the Windows Program Files
     */
    private static String getProgramFilesPath() {
      return getEnvVarPath("ProgramFiles", "C:\\Program Files").replace(" (x86)", "");
    }
  
    private static String getProgramFiles86Path() {
      return getEnvVarPath("ProgramFiles(x86)", "C:\\Program Files (x86)");
    }
  
    private static String getEnvVarPath(final String envVar, final String defaultValue) {
      return getEnvVarIgnoreCase(envVar)
          .map(File::new)
          .filter(File::exists)
          .map(File::getAbsolutePath)
          .orElseGet(() -> new File(defaultValue).getAbsolutePath());
    }
  
    private static Optional<String> getEnvVarIgnoreCase(String var) {
      return System.getenv().entrySet().stream()
          .filter(e -> e.getKey().equalsIgnoreCase(var))
          .findFirst()
          .map(Map.Entry::getValue);
    }
  }
  