blob: 5ff6718bfa3cc68813a565d3f16527fd4ea69bac [file] [log] [blame]
// Copyright (C) 2014 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.server.plugins;
import com.google.common.base.MoreObjects;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.registration.PluginName;
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.plugins.Plugin.ApiType;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.eclipse.jgit.internal.storage.file.FileSnapshot;
@Singleton
public class JarPluginProvider implements ServerPluginProvider {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
static final String PLUGIN_TMP_PREFIX = "plugin_";
static final String JAR_EXTENSION = ".jar";
private final Path tmpDir;
private final PluginConfigFactory configFactory;
private ClassLoader pluginApiClassLoader = PluginUtil.parentFor(ApiType.PLUGIN);
@Inject
JarPluginProvider(SitePaths sitePaths, PluginConfigFactory configFactory) {
this.tmpDir = sitePaths.tmp_dir;
this.configFactory = configFactory;
}
@Override
public boolean handles(Path srcPath) {
String fileName = srcPath.getFileName().toString();
return fileName.endsWith(JAR_EXTENSION) || fileName.endsWith(JAR_EXTENSION + ".disabled");
}
@Override
public String getPluginName(Path srcPath) {
try {
return MoreObjects.firstNonNull(getJarPluginName(srcPath), PluginUtil.nameOf(srcPath));
} catch (IOException e) {
throw new IllegalArgumentException(
"Invalid plugin file " + srcPath + ": cannot get plugin name", e);
}
}
public static String getJarPluginName(Path srcPath) throws IOException {
try (JarFile jarFile = new JarFile(srcPath.toFile())) {
return jarFile.getManifest().getMainAttributes().getValue("Gerrit-PluginName");
}
}
@Override
public ServerPlugin get(Path srcPath, FileSnapshot snapshot, PluginDescription description)
throws InvalidPluginException {
try {
String name = getPluginName(srcPath);
String extension = getExtension(srcPath);
try (InputStream in = Files.newInputStream(srcPath)) {
Path tmp = PluginUtil.asTemp(in, tempNameFor(name), extension, tmpDir);
return loadJarPlugin(name, srcPath, snapshot, tmp, description);
}
} catch (IOException e) {
throw new InvalidPluginException("Cannot load Jar plugin " + srcPath, e);
}
}
@Override
public String getProviderPluginName() {
return PluginName.GERRIT;
}
private static String getExtension(Path path) {
return getExtension(path.getFileName().toString());
}
private static String getExtension(String name) {
int ext = name.lastIndexOf('.');
return 0 < ext ? name.substring(ext) : "";
}
private static String tempNameFor(String name) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyMMdd_HHmm").withZone(ZoneId.of("UTC"));
return PLUGIN_TMP_PREFIX + name + "_" + fmt.format(Instant.now()) + "_";
}
public static Path storeInTemp(String pluginName, InputStream in, SitePaths sitePaths)
throws IOException {
return PluginUtil.asTemp(in, tempNameFor(pluginName), ".jar", sitePaths.tmp_dir);
}
private ServerPlugin loadJarPlugin(
String name, Path srcJar, FileSnapshot snapshot, Path tmp, PluginDescription description)
throws IOException, InvalidPluginException, MalformedURLException {
JarFile jarFile = new JarFile(tmp.toFile());
boolean keep = false;
try {
Manifest manifest = jarFile.getManifest();
Plugin.ApiType type = Plugin.getApiType(manifest);
List<URL> urls = new ArrayList<>(2);
String overlay = System.getProperty("gerrit.plugin-classes");
if (overlay != null) {
Path classes = Path.of(overlay).resolve(name).resolve("main");
if (Files.isDirectory(classes)) {
logger.atInfo().log("plugin %s: including %s", name, classes);
urls.add(classes.toUri().toURL());
}
}
urls.add(tmp.toUri().toURL());
ClassLoader parentClassLoader = parentFor(type);
URLClassLoader pluginLoader =
URLClassLoader.newInstance(urls.toArray(new URL[urls.size()]), parentClassLoader);
if (manifest.getMainAttributes().getValue(ServerPlugin.API_MODULE) != null) {
pluginApiClassLoader = pluginLoader;
}
JarScanner jarScanner = createJarScanner(tmp);
PluginConfig pluginConfig = configFactory.getFromGerritConfig(name);
ServerPlugin plugin =
new ServerPlugin(
name,
description.canonicalUrl,
description.user,
srcJar,
snapshot,
jarScanner,
description.dataDir,
pluginLoader,
pluginConfig.getString("metricsPrefix", null),
description.gerritRuntime);
plugin.setCleanupHandle(new CleanupHandle(tmp, jarFile));
keep = true;
return plugin;
} finally {
if (!keep) {
jarFile.close();
}
}
}
private ClassLoader parentFor(ApiType type) {
switch (type) {
case PLUGIN:
return pluginApiClassLoader;
// $CASES-OMITTED$
default:
return PluginUtil.parentFor(type);
}
}
private JarScanner createJarScanner(Path srcJar) throws InvalidPluginException {
try {
return new JarScanner(srcJar);
} catch (IOException e) {
throw new InvalidPluginException("Cannot scan plugin file " + srcJar, e);
}
}
}