001/* 002 * Copyright 2023 Smartefact 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package smartefact.xdg.basedir; 018 019import java.nio.file.Path; 020import java.util.List; 021import java.util.function.Supplier; 022import java.util.stream.Stream; 023import org.jetbrains.annotations.Contract; 024import org.jetbrains.annotations.NotNull; 025import org.jetbrains.annotations.Nullable; 026import smartefact.Collections; 027import static smartefact.Preconditions.checkNotNull; 028import smartefact.Strings; 029import smartefact.SystemProperties; 030import static smartefact.ToStringBuilder.toStringBuilder; 031 032/** 033 * Utilities for the <em>XDG Base Directory specification</em>. 034 * <p> 035 * <b>Specification:</b> XDG Base Directory 0.8 036 * </p> 037 * 038 * @author Laurent Pireyn 039 * @see <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG Base Directory Specification</a> 040 */ 041public final class XdgBaseDir { 042 private static final String HOME = "HOME"; 043 private static final String XDG_CACHE_HOME = "XDG_CACHE_HOME"; 044 private static final String XDG_CONFIG_DIRS = "XDG_CONFIG_DIRS"; 045 private static final String XDG_CONFIG_HOME = "XDG_CONFIG_HOME"; 046 private static final String XDG_DATA_DIRS = "XDG_DATA_DIRS"; 047 private static final String XDG_DATA_HOME = "XDG_DATA_HOME"; 048 private static final String XDG_RUNTIME_DIR = "XDG_RUNTIME_DIR"; 049 private static final String XDG_STATE_HOME = "XDG_STATE_HOME"; 050 private static final char PATH_SEPARATOR = ':'; 051 052 private static @Nullable XdgBaseDir instance; 053 054 public static @NotNull XdgBaseDir getInstance() { 055 synchronized (XdgBaseDir.class) { 056 if (instance == null) { 057 final @NotNull String fileSeparator = SystemProperties.getFileSeparator(); 058 if (!fileSeparator.equals("/")) { 059 throw new IllegalStateException( 060 "The current platform does not support the XDG Base Directory specification, " + 061 "as it uses <" + fileSeparator + "> as file separator" 062 ); 063 } 064 instance = new XdgBaseDir(); 065 } 066 return instance; 067 } 068 } 069 070 @Contract(pure = true) 071 private static @Nullable String getEnvVar(@NotNull String name) { 072 final @Nullable String value; 073 try { 074 value = System.getenv(name); 075 } catch (SecurityException e) { 076 return null; 077 } catch (NullPointerException e) { 078 throw new AssertionError(e); 079 } 080 return !Strings.isNullOrBlank(value) ? value : null; 081 } 082 083 @Contract(pure = true) 084 private static @NotNull String getRequiredEnvVar(@NotNull String name) { 085 return checkNotNull( 086 getEnvVar(name), 087 () -> "The environment variable <" + name + "> is undefined" 088 ); 089 } 090 091 @Contract(pure = true) 092 private static @Nullable Path getEnvVarAsPath(@NotNull String name) { 093 final @Nullable String value = getEnvVar(name); 094 return value != null ? Path.of(value) : null; 095 } 096 097 @Contract(pure = true) 098 private static @NotNull Path getEnvVarAsPath( 099 @NotNull String name, 100 @NotNull Supplier<? extends @NotNull Path> defaultValue 101 ) { 102 final @Nullable Path value = getEnvVarAsPath(name); 103 return value != null ? value : defaultValue.get(); 104 } 105 106 @Contract(pure = true) 107 private static @NotNull List<@NotNull Path> getEnvVarAsPathList( 108 @NotNull String name, 109 @NotNull Supplier<? extends @NotNull List<@NotNull Path>> defaultValue 110 ) { 111 final @Nullable String value = getEnvVar(name); 112 return value != null ? toPathList(value) : defaultValue.get(); 113 } 114 115 @Contract(pure = true) 116 private static @NotNull List<@NotNull Path> toPathList(@NotNull CharSequence string) { 117 return Collections.map( 118 Strings.split(string, PATH_SEPARATOR), 119 Path::of 120 ); 121 } 122 123 @Contract(pure = true) 124 private static @NotNull Stream<@NotNull Path> getExistingFiles( 125 @NotNull Stream<? extends @NotNull Path> candidateDirs, 126 @NotNull String dirName, 127 @NotNull String fileName 128 ) { 129 final @NotNull Path filePath = Path.of(dirName, fileName); 130 return candidateDirs 131 .map(dir -> dir.resolve(filePath)) 132 .filter(file -> file.toFile().exists()); 133 } 134 135 private final @NotNull Path home = Path.of(getRequiredEnvVar(HOME)); 136 137 private final @NotNull Path cacheHome = getEnvVarAsPath( 138 XDG_CACHE_HOME, 139 () -> home.resolve(".cache") 140 ); 141 142 private final @NotNull Path configHome = getEnvVarAsPath( 143 XDG_CONFIG_HOME, 144 () -> home.resolve(".config") 145 ); 146 147 private final @NotNull Path dataHome = getEnvVarAsPath( 148 XDG_DATA_HOME, 149 () -> home.resolve(".local/share") 150 ); 151 152 private final @NotNull Path stateHome = getEnvVarAsPath( 153 XDG_STATE_HOME, 154 () -> home.resolve(".local/state") 155 ); 156 157 private final @Nullable Path runtimeDir = getEnvVarAsPath(XDG_RUNTIME_DIR); 158 159 private final @NotNull Path binDir = home.resolve(".local/bin"); 160 161 private final @NotNull List<@NotNull Path> configDirs = getEnvVarAsPathList( 162 XDG_CONFIG_DIRS, 163 () -> List.of(Path.of("/etc/xdg")) 164 ); 165 166 private final @NotNull List<@NotNull Path> dataDirs = getEnvVarAsPathList( 167 XDG_DATA_DIRS, 168 () -> List.of(Path.of("/usr/local/share"), Path.of("/usr/share")) 169 ); 170 171 private XdgBaseDir() {} 172 173 /** 174 * Returns the home directory. 175 * <p> 176 * This implementation is based on the {@code HOME} environment variable, 177 * not on the {@code user.home} system property 178 * (although they should have the same value). 179 * </p> 180 * 181 * @return the home directory (never {@code null}) 182 */ 183 @Contract(pure = true) 184 public @NotNull Path getHome() { 185 return home; 186 } 187 188 /** 189 * Returns the cached data directory. 190 * 191 * @return the cached data directory (never {@code null}) 192 */ 193 @Contract(pure = true) 194 public @NotNull Path getCacheHome() { 195 return cacheHome; 196 } 197 198 /** 199 * Returns the configuration directory. 200 * 201 * @return the configuration directory (never {@code null}) 202 */ 203 @Contract(pure = true) 204 public @NotNull Path getConfigHome() { 205 return configHome; 206 } 207 208 /** 209 * Returns the data directory. 210 * 211 * @return the data directory (never {@code null}) 212 */ 213 @Contract(pure = true) 214 public @NotNull Path getDataHome() { 215 return dataHome; 216 } 217 218 /** 219 * Returns the state data directory. 220 * 221 * @return the state data directory (never {@code null}) 222 */ 223 @Contract(pure = true) 224 public @NotNull Path getStateHome() { 225 return stateHome; 226 } 227 228 /** 229 * Returns the runtime directory. 230 * <p> 231 * This may be {@code null} as it has no default value. 232 * </p> 233 * 234 * @return the runtime directory (maybe {@code null}) 235 */ 236 @Contract(pure = true) 237 public @Nullable Path getRuntimeDir() { 238 return runtimeDir; 239 } 240 241 /** 242 * Returns the user binary directory. 243 * 244 * @return the user binary directory (never {@code null}) 245 */ 246 @Contract(pure = true) 247 public @NotNull Path getBinDir() { 248 return binDir; 249 } 250 251 /** 252 * Returns the configuration directories. 253 * 254 * @return the configuration directories (never {@code null}) 255 */ 256 @Contract(pure = true) 257 public @NotNull List<@NotNull Path> getConfigDirs() { 258 return configDirs; 259 } 260 261 /** 262 * Returns the data directories. 263 * 264 * @return the data directories (never {@code null}) 265 */ 266 @Contract(pure = true) 267 public @NotNull List<@NotNull Path> getDataDirs() { 268 return dataDirs; 269 } 270 271 @Contract(pure = true) 272 public @NotNull Stream<@NotNull Path> getCandidateConfigDirs() { 273 return Stream.concat( 274 Stream.of(configHome), 275 configDirs.stream() 276 ); 277 } 278 279 @Contract(pure = true) 280 public @NotNull Stream<@NotNull Path> getCandidateDataDirs() { 281 return Stream.concat( 282 Stream.of(dataHome), 283 dataDirs.stream() 284 ); 285 } 286 287 @Contract(pure = true) 288 public @NotNull Stream<@NotNull Path> getConfigFiles( 289 @NotNull String dirName, 290 @NotNull String fileName 291 ) { 292 return getExistingFiles( 293 getCandidateConfigDirs(), 294 dirName, 295 fileName 296 ); 297 } 298 299 @Contract(pure = true) 300 public @NotNull Stream<@NotNull Path> getDataFiles( 301 @NotNull String dirName, 302 @NotNull String fileName 303 ) { 304 return getExistingFiles( 305 getCandidateDataDirs(), 306 dirName, 307 fileName 308 ); 309 } 310 311 @Override 312 @Contract(pure = true) 313 public String toString() { 314 return toStringBuilder(this) 315 .property("home", home) 316 .property("cacheHome", cacheHome) 317 .property("configHome", configHome) 318 .property("dataHome", dataHome) 319 .property("stateHome", stateHome) 320 .property("runtimeDir", runtimeDir) 321 .property("binDir", binDir) 322 .property("configDirs", configDirs) 323 .property("dataDirs", dataDirs) 324 .build(); 325 } 326}