DefaultPluginsDir.java
/*
* 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.tika.pipes.core.config;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Set;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resolves the default {@code plugins} directory when {@code plugin-roots}
* is not configured (TIKA-4864). The probe order matches the install
* layouts Tika ships:
* <ol>
* <li>next to the jar the anchor class was loaded from (a flat install,
* tika-grpc's docker image);</li>
* <li>next to that directory's parent (the unpacked distributions and the
* tika-server docker image load classes from {@code lib/}, the plugins
* sit beside {@code lib/});</li>
* <li>a {@code plugins} directory in the current working directory.</li>
* </ol>
* The result is always absolute: the forked pipes server resolves the
* configured value against its own working directory, which need not be the
* parent's, so a relative default would make the two processes disagree.
*/
public final class DefaultPluginsDir {
private static final Logger LOG = LoggerFactory.getLogger(DefaultPluginsDir.class);
/**
* The directory name probed in each location.
*/
public static final String PLUGINS_DIR_NAME = "plugins";
// tokens the example configs shown in the docs carry for plugin-roots
private static final Set<String> PLACEHOLDERS = Set.of("PLUGINS_PATHS", "PLUGIN_ROOTS");
private DefaultPluginsDir() {
}
/**
* Resolves the default plugins directory for the install layout of the
* given class.
*
* @param anchor the class whose code source anchors the probe, usually
* the caller
* @return the absolute path of the first {@code plugins} directory found,
* or the absolute path of {@code plugins} in the working directory if
* none exists yet
*/
public static String resolve(Class<?> anchor) {
return resolve(codeSourceDir(anchor), Path.of("")).toString();
}
/**
* The same probe as {@link #resolve(Class)}, but empty rather than a warning and a
* fallback path when no {@code plugins} directory exists.
*/
public static Optional<Path> find(Class<?> anchor) {
return find(codeSourceDir(anchor), Path.of(""));
}
/**
* @param pluginRoots the configured {@code plugin-roots} node, or null
* @return true when it is absent or still a placeholder token copied from an example
* config, so the caller should install the default
*/
public static boolean needsDefault(JsonNode pluginRoots) {
return pluginRoots == null || pluginRoots.isNull()
|| (pluginRoots.isTextual() && PLACEHOLDERS.contains(pluginRoots.asText()));
}
private static Path codeSourceDir(Class<?> anchor) {
try {
return Path.of(anchor.getProtectionDomain().getCodeSource().getLocation()
.toURI()).getParent();
} catch (Exception e) {
//no code source (e.g. a repacked classloader): probe the working
//directory only
return null;
}
}
/**
* The probe itself, separated from the code-source lookup for testing.
*
* @param codeSourceDir the directory holding the anchor's jar, or null
* @param cwd the working directory to fall back to
* @return the absolute path of the resolved directory
*/
public static Path resolve(Path codeSourceDir, Path cwd) {
Path fallback = cwd.resolve(PLUGINS_DIR_NAME).toAbsolutePath();
return find(codeSourceDir, cwd).orElseGet(() -> {
LOG.warn("no plugins directory found in the install layout or at {}; "
+ "pipes plugins will not load unless plugin-roots is configured", fallback);
return fallback;
});
}
/**
* @return the first existing {@code plugins} directory: beside the code source, beside its
* parent, or in {@code cwd}; empty when there is none
*/
public static Optional<Path> find(Path codeSourceDir, Path cwd) {
if (codeSourceDir != null) {
Path nextToJar = codeSourceDir.resolve(PLUGINS_DIR_NAME);
if (Files.isDirectory(nextToJar)) {
return Optional.of(nextToJar.toAbsolutePath());
}
Path parent = codeSourceDir.getParent();
if (parent != null) {
Path nextToParent = parent.resolve(PLUGINS_DIR_NAME);
if (Files.isDirectory(nextToParent)) {
return Optional.of(nextToParent.toAbsolutePath());
}
}
}
Path inCwd = cwd.resolve(PLUGINS_DIR_NAME).toAbsolutePath();
return Files.isDirectory(inCwd) ? Optional.of(inCwd) : Optional.empty();
}
}