/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.enterprise.server.core.plugin;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.net.URL;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.maven.artifact.versioning.ComparableVersion;
import org.jetbrains.annotations.Nullable;
import org.rhq.core.domain.cloud.Server;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.definition.ConfigurationDefinition;
import org.rhq.core.domain.configuration.definition.ConfigurationTemplate;
import org.rhq.core.domain.plugin.PluginKey;
import org.rhq.core.domain.plugin.PluginStatusType;
import org.rhq.core.domain.plugin.ServerPlugin;
import org.rhq.core.util.MessageDigestGenerator;
import org.rhq.core.util.jdbc.JDBCUtil;
import org.rhq.core.util.stream.StreamUtil;
import org.rhq.enterprise.server.auth.SubjectManagerLocal;
import org.rhq.enterprise.server.cloud.instance.ServerManagerLocal;
import org.rhq.enterprise.server.plugin.ServerPluginManagerLocal;
import org.rhq.enterprise.server.plugin.pc.AbstractTypeServerPluginContainer;
import org.rhq.enterprise.server.plugin.pc.MasterServerPluginContainer;
import org.rhq.enterprise.server.plugin.pc.ServerPluginType;
import org.rhq.enterprise.server.util.LookupUtil;
import org.rhq.enterprise.server.xmlschema.ServerPluginDescriptorMetadataParser;
import org.rhq.enterprise.server.xmlschema.ServerPluginDescriptorUtil;
import org.rhq.enterprise.server.xmlschema.generated.serverplugin.ServerPluginDescriptorType;
/**
* This looks at both the file system and the database for new server plugins.
*
* @author John Mazzitelli
*/
public class ServerPluginScanner {
private Log log = LogFactory.getLog(ServerPluginScanner.class);
/** a list of server plugins found on previous scans that have not yet been processed */
private List<File> scanned = new ArrayList<File>();
/** Maintains a cache of what we had on the filesystem during the last scan */
private Map<File, PluginWithDescriptor> serverPluginsOnFilesystem = new HashMap<File, PluginWithDescriptor>();
/** directory where the server plugin jar files are found */
private File serverPluginDir;
public ServerPluginScanner() {
}
public File getServerPluginDir() {
return this.serverPluginDir;
}
public void setServerPluginDir(File dir) {
this.serverPluginDir = dir;
}
/**
* This should be called after a call to {@link #serverPluginScan()} to register
* plugins that were found in the scan.
*
* This will also check to see if previously registered plugins changed their state.
*
* @throws Exception on unexpected errors
*/
void registerServerPlugins() throws Exception {
for (File file : this.scanned) {
log.debug("Deploying server plugin [" + file + "]...");
registerServerPlugin(file);
}
this.scanned.clear();
// we now have to see if the state of existing plugins have changed in the DB
ServerPluginManagerLocal serverPluginsManager = LookupUtil.getServerPluginManager();
List<ServerPlugin> allPlugins = serverPluginsManager.getAllServerPlugins();
List<ServerPlugin> installedPlugins = new ArrayList<ServerPlugin>();
List<ServerPlugin> undeployedPlugins = new ArrayList<ServerPlugin>();
for (ServerPlugin plugin : allPlugins) {
if (plugin.getStatus() == PluginStatusType.INSTALLED) {
installedPlugins.add(plugin);
} else {
undeployedPlugins.add(plugin);
}
}
// first, have any plugins been undeployed since we last checked?
for (ServerPlugin undeployedPlugin : undeployedPlugins) {
File undeployedFile = new File(this.getServerPluginDir(), undeployedPlugin.getPath());
PluginWithDescriptor pluginWithDescriptor = this.serverPluginsOnFilesystem.get(undeployedFile);
if (pluginWithDescriptor != null) {
try {
log.info("Plugin file [" + undeployedFile + "] has been undeployed. It will be deleted.");
List<Integer> id = Arrays.asList(undeployedPlugin.getId());
serverPluginsManager.deleteServerPlugins(LookupUtil.getSubjectManager().getOverlord(), id);
} finally {
this.serverPluginsOnFilesystem.remove(undeployedFile);
}
}
}
// now see if any plugins changed state from enabled->disabled or vice versa
// this also checks to see if the plugin configuration has changed since it was last loaded
MasterServerPluginContainer master = LookupUtil.getServerPluginService().getMasterPluginContainer();
if (master != null) {
for (ServerPlugin installedPlugin : installedPlugins) {
PluginKey key = PluginKey.createServerPluginKey(installedPlugin.getType(), installedPlugin.getName());
AbstractTypeServerPluginContainer pc = master.getPluginContainerByPlugin(key);
if (pc != null && pc.isPluginLoaded(key)) {
boolean needToReloadPlugin = false;
boolean currentlyEnabled = pc.isPluginEnabled(key);
if (installedPlugin.isEnabled() != currentlyEnabled) {
log.info("Detected a state change to plugin [" + key + "]. It will now be "
+ ((installedPlugin.isEnabled()) ? "[enabled]" : "[disabled]"));
needToReloadPlugin = true;
} else {
Long pluginLoadTime = pc.getPluginLoadTime(key);
if (pluginLoadTime != null) {
long configChangeTimestamp = serverPluginsManager
.getLastConfigurationChangeTimestamp(installedPlugin.getId());
if (configChangeTimestamp > pluginLoadTime) {
// since the last time the plugin was loaded, its configuration has changed, reload it to pick up the new config
log.info("Detected a config change to plugin [" + key + "]. It will be reloaded and "
+ ((installedPlugin.isEnabled()) ? "[enabled]" : "[disabled]"));
needToReloadPlugin = true;
}
}
}
if (needToReloadPlugin) {
pc.reloadPlugin(key, installedPlugin.isEnabled());
}
}
}
}
return;
}
/**
* This method just scans the filesystem and DB for server plugin changes but makes
* no attempt to register the plugins.
*
* @throws Exception on unexpected errors
*/
void serverPluginScan() throws Exception {
log.debug("Scanning for server plugins");
if (this.getServerPluginDir() == null || !this.getServerPluginDir().isDirectory()) {
// nothing to do since there is no plugin directory configured
return;
}
// ensure that the filesystem and database are in a consistent state
List<File> updatedFiles1 = serverPluginScanFilesystem();
List<File> updatedFiles2 = serverPluginScanDatabase();
// process any newly detected plugins
List<File> allUpdatedFiles = new ArrayList<File>();
allUpdatedFiles.addAll(updatedFiles1);
allUpdatedFiles.addAll(updatedFiles2);
for (File updatedFile : allUpdatedFiles) {
log.debug("Scan detected server plugin [" + updatedFile + "]...");
this.scanned.add(updatedFile);
}
ServerPluginManagerLocal pluginMgr = LookupUtil.getServerPluginManager();
ServerManagerLocal serverMgr = LookupUtil.getServerManager();
Server thisServer = serverMgr.getServer();
pluginMgr.acknowledgeDeletedPluginsBy(thisServer.getId());
}
/**
* This is called when a server plugin jar has been found on the filesystem that hasn't been seen yet
* during this particular lifetime of the scanner. This does not necessarily mean its a new plugin jar,
* it only means this is the first time we've seen it since this object has been instantiated.
* This method will check to see if the database record matches the new plugin file and if so, does nothing.
*
* @param pluginFile the new server plugin file
*/
private void registerServerPlugin(File pluginFile) {
try {
ServerPluginDescriptorType descriptor;
descriptor = this.serverPluginsOnFilesystem.get(pluginFile).descriptor;
String pluginName = descriptor.getName();
String displayName = descriptor.getDisplayName();
ComparableVersion version; // this must be non-null, the next line ensures this
version = ServerPluginDescriptorUtil.getPluginVersion(pluginFile, descriptor);
log.debug("Registering server plugin [" + pluginName + "], version " + version);
ServerPlugin plugin = new ServerPlugin(pluginName, pluginFile.getName());
plugin.setDisplayName((displayName != null) ? displayName : pluginName);
plugin.setEnabled(!descriptor.isDisabledOnDiscovery());
plugin.setDescription(descriptor.getDescription());
plugin.setMtime(pluginFile.lastModified());
plugin.setVersion(version.toString());
plugin.setAmpsVersion(descriptor.getApiVersion());
plugin.setMD5(MessageDigestGenerator.getDigestString(pluginFile));
plugin.setPluginConfiguration(getDefaultPluginConfiguration(descriptor));
plugin.setScheduledJobsConfiguration(getDefaultScheduledJobsConfiguration(descriptor));
plugin.setType(new ServerPluginType(descriptor).stringify());
if (descriptor.getHelp() != null && !descriptor.getHelp().getContent().isEmpty()) {
plugin.setHelp(String.valueOf(descriptor.getHelp().getContent().get(0)));
}
ServerPluginManagerLocal serverPluginsManager = LookupUtil.getServerPluginManager();
// see if this plugin has been deleted previously; if so, don't register and delete the file
PluginKey newPluginKey = new PluginKey(plugin);
PluginStatusType status = serverPluginsManager.getServerPluginStatus(newPluginKey);
if (PluginStatusType.DELETED == status) {
log.warn("Plugin file [" + pluginFile + "] has been detected but that plugin with name [" + pluginName
+ "] was previously undeployed. Will not re-register that plugin and the file will be deleted.");
boolean succeeded = pluginFile.delete();
if (!succeeded) {
log.error("Failed to delete obsolete plugin file [" + pluginFile + "].");
}
} else {
// now attempt to register the plugin. "dbPlugin" will be the new updated plugin; but if
// the scanned plugin does not obsolete the current plugin, then dbPlugin will be the old, still valid, plugin.
SubjectManagerLocal subjectManager = LookupUtil.getSubjectManager();
ServerPlugin dbPlugin = serverPluginsManager.registerServerPlugin(subjectManager.getOverlord(), plugin,
pluginFile);
log.info("Registered server plugin [" + dbPlugin.getName() + "], version " + dbPlugin.getVersion());
}
} catch (Exception e) {
log.error("Failed to register server plugin file [" + pluginFile + "]", e);
}
return;
}
private Configuration getDefaultPluginConfiguration(ServerPluginDescriptorType descriptor) throws Exception {
Configuration defaults = null;
ConfigurationDefinition def = ServerPluginDescriptorMetadataParser.getPluginConfigurationDefinition(descriptor);
if (def != null) {
defaults = createDefaultConfiguration(def);
}
return defaults;
}
private Configuration getDefaultScheduledJobsConfiguration(ServerPluginDescriptorType descriptor) throws Exception {
Configuration defaults = null;
ConfigurationDefinition def = ServerPluginDescriptorMetadataParser.getScheduledJobsDefinition(descriptor);
if (def != null) {
defaults = createDefaultConfiguration(def);
}
return defaults;
}
private Configuration createDefaultConfiguration(ConfigurationDefinition def) {
ConfigurationTemplate defaultTemplate = def.getDefaultTemplate();
return (defaultTemplate != null) ? defaultTemplate.createConfiguration() : new Configuration();
}
/**
* Scans the plugin directory and updates our cache of known plugin files.
* This will purge any old plugins that are deemed obsolete.
*
* @return a list of files that appear to be new or updated and should be deployed
*/
private List<File> serverPluginScanFilesystem() {
List<File> updated = new ArrayList<File>();
// get the current list of plugins deployed on the filesystem
File[] pluginJars = this.getServerPluginDir().listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.endsWith(".jar");
}
});
// refresh our cache so it reflects what is currently on the filesystem
// first we remove any jar files in our cache that we no longer have on the filesystem
ArrayList<File> doomedPluginFiles = new ArrayList<File>();
for (File cachedPluginFile : this.serverPluginsOnFilesystem.keySet()) {
boolean existsOnFileSystem = false;
for (File filesystemPluginFile : pluginJars) {
if (cachedPluginFile.equals(filesystemPluginFile)) {
existsOnFileSystem = true;
break; // our cached jar still exists on the file system
}
}
if (!existsOnFileSystem) {
doomedPluginFiles.add(cachedPluginFile); // this plugin file has been deleted from the filesystem, remove it from the cache
}
}
for (File deletedPluginFile : doomedPluginFiles) {
this.serverPluginsOnFilesystem.remove(deletedPluginFile);
}
// now insert new cache items representing new jar files and update existing ones as appropriate
for (File pluginJar : pluginJars) {
String md5 = null;
PluginWithDescriptor pluginWithDescriptor = this.serverPluginsOnFilesystem.get(pluginJar);
ServerPlugin plugin = null;
if (pluginWithDescriptor != null) {
plugin = pluginWithDescriptor.plugin;
}
try {
if (plugin != null) {
if (pluginJar.lastModified() == 0L) {
// for some reason the operating system can't give us the last mod time, we need to do MD5 check
md5 = MessageDigestGenerator.getDigestString(pluginJar);
if (!md5.equals(plugin.getMd5())) {
plugin = null; // this plugin jar has changed - force it to refresh the cache.
}
} else if (pluginJar.lastModified() != plugin.getMtime()) {
plugin = null; // this plugin jar has changed - force it to refresh the cache.
}
}
if (plugin == null) {
cacheFilesystemServerPluginJar(pluginJar, md5);
updated.add(pluginJar);
}
} catch (Exception e) {
log.warn("Failed to scan server plugin [" + pluginJar + "] found on filesystem. Skipping. Cause: " + e);
this.serverPluginsOnFilesystem.remove(pluginJar); // act like we never saw it
updated.remove(pluginJar);
}
}
// Let's check to see if there are any obsolete plugins that need to be deleted.
// This is needed if plugin-A-1.0.jar exists and someone deployed plugin-A-1.1.jar but fails to delete plugin-A-1.0.jar.
doomedPluginFiles.clear();
HashMap<String, ServerPlugin> pluginsByName = new HashMap<String, ServerPlugin>(); // key on (name+type), not just name
for (Entry<File, PluginWithDescriptor> currentPluginFileEntry : this.serverPluginsOnFilesystem.entrySet()) {
ServerPlugin currentPlugin = currentPluginFileEntry.getValue().plugin;
ServerPlugin existingPlugin = pluginsByName.get(currentPlugin.getName() + currentPlugin.getType());
if (existingPlugin == null) {
// this is the usual case - this is the only plugin with the given name we've seen
pluginsByName.put(currentPlugin.getName() + currentPlugin.getType(), currentPlugin);
} else {
ServerPlugin obsolete = ServerPluginDescriptorUtil.determineObsoletePlugin(currentPlugin,
existingPlugin);
if (obsolete == null) {
obsolete = currentPlugin; // both were identical, but we only want one file so pick one to get rid of
}
doomedPluginFiles.add(new File(this.getServerPluginDir(), obsolete.getPath()));
if (obsolete == existingPlugin) { // yes use == for reference equality!
pluginsByName.put(currentPlugin.getName() + currentPlugin.getType(), currentPlugin); // override the original one we saw with this latest one
}
}
}
// now we need to actually delete any obsolete plugin files from the file system
for (File doomedPluginFile : doomedPluginFiles) {
if (doomedPluginFile.delete()) {
log.info("Deleted an obsolete server plugin file: " + doomedPluginFile);
this.serverPluginsOnFilesystem.remove(doomedPluginFile);
updated.remove(doomedPluginFile);
} else {
log.warn("Failed to delete what was deemed an obsolete server plugin file: " + doomedPluginFile);
}
}
return updated;
}
/**
* Creates a {@link ServerPlugin} object for the given plugin jar and caches it.
* @param pluginJar information about this plugin jar will be cached
* @param md5 if known, this is the plugin jar's MD5, <code>null</code> if not known
* @return the plugin jar file's information that has been cached
* @throws Exception if failed to get information about the plugin
*/
private ServerPlugin cacheFilesystemServerPluginJar(File pluginJar, @Nullable String md5) throws Exception {
if (md5 == null) { // don't calculate the MD5 is we've already done it before
md5 = MessageDigestGenerator.getDigestString(pluginJar);
}
URL pluginUrl = pluginJar.toURI().toURL();
ServerPluginDescriptorType descriptor = ServerPluginDescriptorUtil.loadPluginDescriptorFromUrl(pluginUrl);
String version = ServerPluginDescriptorUtil.getPluginVersion(pluginJar, descriptor).toString();
String name = descriptor.getName();
ServerPlugin plugin = new ServerPlugin(name, pluginJar.getName());
plugin.setType(new ServerPluginType(descriptor).stringify());
plugin.setMd5(md5);
plugin.setVersion(version);
plugin.setMtime(pluginJar.lastModified());
this.serverPluginsOnFilesystem.put(pluginJar, new PluginWithDescriptor(plugin, descriptor));
return plugin;
}
/**
* This method scans the database for any new or updated server plugins and make sure this server
* has a plugin file on the filesystem for each of those new/updated server plugins.
*
* This also checks to see if the enabled flag changed for plugins that we already know about.
* If it does, and its plugin container has the plugin already loaded, the plugin will be reloaded.
*
* @return a list of files that appear to be new or updated and should be deployed
*/
private List<File> serverPluginScanDatabase() throws Exception {
// these are plugins (name/path/md5/mtime) that have changed in the DB but are missing from the file system
List<ServerPlugin> updatedPlugins = new ArrayList<ServerPlugin>();
// the same list as above, only they are the files that are written to the filesystem and no longer missing
List<File> updatedFiles = new ArrayList<File>();
// process all the installed plugins
ServerPluginManagerLocal serverPluginsManager = LookupUtil.getServerPluginManager();
List<ServerPlugin> installedPlugins = serverPluginsManager.getServerPlugins();
for (ServerPlugin installedPlugin : installedPlugins) {
String name = installedPlugin.getName();
String path = installedPlugin.getPath();
String md5 = installedPlugin.getMd5();
long mtime = installedPlugin.getMtime();
String version = installedPlugin.getVersion();
ServerPluginType pluginType = new ServerPluginType(installedPlugin.getType());
// let's see if we have this logical plugin on the filesystem (it may or may not be under the same filename)
File expectedFile = new File(this.getServerPluginDir(), path);
File currentFile = null; // will be non-null if we find that we have this plugin on the filesystem already
PluginWithDescriptor pluginWithDescriptor = this.serverPluginsOnFilesystem.get(expectedFile);
if (pluginWithDescriptor != null) {
currentFile = expectedFile; // we have it where we are expected to have it
if (!pluginWithDescriptor.plugin.getName().equals(name)
|| !pluginType.equals(pluginWithDescriptor.pluginType)) {
// Happens if someone wrote a plugin of one type but later changed it to a different type (or changed names)
log.warn("For some reason, the server plugin file [" + expectedFile + "] is plugin ["
+ pluginWithDescriptor.plugin.getName() + "] of type [" + pluginWithDescriptor.pluginType
+ "] but the database says it should be [" + name + "] of type [" + pluginType + "]");
} else {
log.debug("File system and db agree on server plugin location for [" + expectedFile + "]");
}
} else {
// the plugin might still be on the file system but under a different filename, see if we can find it
for (Map.Entry<File, PluginWithDescriptor> cacheEntry : this.serverPluginsOnFilesystem.entrySet()) {
if (cacheEntry.getValue().plugin.getName().equals(name)
&& cacheEntry.getValue().pluginType.equals(pluginType)) {
currentFile = cacheEntry.getKey();
pluginWithDescriptor = cacheEntry.getValue();
log.info("Filesystem has a server plugin [" + name + "] at the file [" + currentFile
+ "] which is different than where the DB thinks it should be [" + expectedFile + "]");
break; // we found it, no need to continue the loop
}
}
}
if (pluginWithDescriptor != null && currentFile != null && currentFile.exists()) {
ServerPlugin dbPlugin = new ServerPlugin(name, path);
dbPlugin.setType(pluginType.stringify());
dbPlugin.setMd5(md5);
dbPlugin.setVersion(version);
dbPlugin.setMtime(mtime);
ServerPlugin obsoletePlugin = ServerPluginDescriptorUtil.determineObsoletePlugin(dbPlugin,
pluginWithDescriptor.plugin);
if (obsoletePlugin == pluginWithDescriptor.plugin) { // yes use == for reference equality!
StringBuilder logMsg = new StringBuilder();
logMsg.append("Found server plugin [").append(name);
logMsg.append("] in the DB that is newer than the one on the filesystem: ");
logMsg.append("DB path=[").append(path);
logMsg.append("]; file path=[").append(currentFile.getName());
logMsg.append("]; DB MD5=[").append(md5);
logMsg.append("]; file MD5=[").append(pluginWithDescriptor.plugin.getMd5());
logMsg.append("]; DB version=[").append(version);
logMsg.append("]; file version=[").append(pluginWithDescriptor.plugin.getVersion());
logMsg.append("]; DB timestamp=[").append(new Date(mtime));
logMsg.append("]; file timestamp=[").append(new Date(pluginWithDescriptor.plugin.getMtime()));
logMsg.append("]");
log.info(logMsg.toString());
updatedPlugins.add(dbPlugin);
if (currentFile.delete()) {
log.info("Deleted the obsolete server plugin file to be updated: " + currentFile);
this.serverPluginsOnFilesystem.remove(currentFile);
} else {
log.warn("Failed to delete the obsolete (to-be-updated) server plugin: " + currentFile);
}
} else if (obsoletePlugin == null) {
// the db is up-to-date, but update the cache so we don't check MD5 or parse the descriptor again
boolean succeeded = currentFile.setLastModified(mtime);
if (!succeeded && log.isDebugEnabled()) {
log.debug("Failed to set mtime to [" + new Date(mtime) + "] on file [" + currentFile + "].");
}
pluginWithDescriptor.plugin.setMtime(mtime);
pluginWithDescriptor.plugin.setVersion(version);
pluginWithDescriptor.plugin.setMd5(md5);
} else {
log.info("It appears that the server plugin [" + dbPlugin
+ "] in the database may be obsolete. If so, it will be updated later.");
}
} else {
log.info("Found server plugin in the DB that we do not yet have: " + name);
ServerPlugin plugin = new ServerPlugin(name, path, md5);
plugin.setType(pluginType.stringify());
plugin.setMtime(mtime);
plugin.setVersion(version);
updatedPlugins.add(plugin);
this.serverPluginsOnFilesystem.remove(expectedFile); // paranoia, make sure the cache doesn't have this
}
}
// write all our updated plugins to the file system
if (!updatedPlugins.isEmpty()) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
DataSource ds = LookupUtil.getDataSource();
conn = ds.getConnection();
ps = conn.prepareStatement("SELECT CONTENT FROM " + ServerPlugin.TABLE_NAME
+ " WHERE DEPLOYMENT = 'SERVER' AND STATUS = 'INSTALLED' AND NAME = ? AND PTYPE = ?");
for (ServerPlugin plugin : updatedPlugins) {
File file = new File(this.getServerPluginDir(), plugin.getPath());
ps.setString(1, plugin.getName());
ps.setString(2, plugin.getType());
rs = ps.executeQuery();
rs.next();
InputStream content = rs.getBinaryStream(1);
StreamUtil.copy(content, new FileOutputStream(file));
rs.close();
boolean succeeded = file.setLastModified(plugin.getMtime());// so our file matches the database mtime
if (!succeeded && log.isDebugEnabled()) {
log.debug("Failed to set mtime to [" + plugin.getMtime() + "] on file [" + file + "].");
}
updatedFiles.add(file);
// we are writing a new file to the filesystem, cache it since we know about it now
cacheFilesystemServerPluginJar(file, null);
}
} finally {
JDBCUtil.safeClose(conn, ps, rs);
}
}
return updatedFiles;
}
private class PluginWithDescriptor {
public PluginWithDescriptor(ServerPlugin plugin, ServerPluginDescriptorType descriptor) {
this.plugin = plugin;
this.descriptor = descriptor;
this.pluginType = new ServerPluginType(descriptor);
}
public final ServerPlugin plugin;
public final ServerPluginDescriptorType descriptor;
public final ServerPluginType pluginType;
}
}