Skip to content

Commit

Permalink
Added a REST API for retrieving list plugin list [comixed#372]
Browse files Browse the repository at this point in the history
  • Loading branch information
mcpierce committed Jul 5, 2020
1 parent 1dc8858 commit 59d51af
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 54 deletions.
3 changes: 3 additions & 0 deletions comixed-library/src/main/java/org/comixed/views/View.java
Expand Up @@ -59,4 +59,7 @@ public interface DuplicatePageList {}

/** Used when fetching library updates. */
public interface LibraryUpdate {}

/** Used when viewing the list of plugins. */
public interface PluginList {}
}
Expand Up @@ -19,16 +19,14 @@
package org.comixed.plugins;

import java.io.*;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.comixed.plugins.model.Plugin;
import org.comixed.plugins.model.PluginDescriptor;
import org.comixed.utils.FileTypeIdentifier;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectFactory;
Expand All @@ -38,7 +36,7 @@
import org.springframework.stereotype.Component;

/**
* <code>PluginManager</code> handles retrieving and launching the plugins.
* <code>PluginManager</code> loads plugins from disk.
*
* @author Darryl L. Pierce
*/
Expand All @@ -53,7 +51,7 @@ public class PluginManager implements InitializingBean {
String pluginLocation;

/** the key is the plugin name, the value is a map of a plugin filename to the file's contents */
Map<String, Map<String, byte[]>> plugins = new HashMap<>();
Map<String, PluginDescriptor> plugins = new HashMap<>();

/**
* Returns a {@link Plugin} by name.
Expand All @@ -64,12 +62,9 @@ public class PluginManager implements InitializingBean {
*/
public Plugin loadPlugin(String name) throws PluginException {
log.debug("Loading plugin details");
Map<String, byte[]> pluginEntries = this.plugins.get(name);
if (pluginEntries == null) throw new PluginException("no such plugin: " + name);
log.debug("Rehydrating plugin");
Plugin result = this.pluginObjectFactory.getObject();
result.setEntries(pluginEntries);
return result;
PluginDescriptor descriptor = this.plugins.get(name);
if (descriptor == null) throw new PluginException("no such plugin: " + name);
return descriptor.getPlugin();
}

@Override
Expand Down Expand Up @@ -141,6 +136,22 @@ private void loadPluginDetails(File pluginFile) throws PluginException {
plugin.setEntries(pluginEntries);
if (this.plugins.containsKey(plugin.getName()))
throw new PluginException("plugin already exists with name: " + plugin.getName());
this.plugins.put(plugin.getName(), pluginEntries);
this.plugins.put(plugin.getName(), plugin.getDescriptor());
}

/**
* Returns the list of all currently loaded plugins.
*
* @return the plugins
*/
public List<PluginDescriptor> getPluginList() {
log.debug("Returning the list of plugins");
List<PluginDescriptor> result = new ArrayList<>();
for (PluginDescriptor value : this.plugins.values()) {
log.debug("Adding plugin: {}", value.getName());
result.add(value);
}
log.debug("Returning {} total plugin{}", result.size(), result.size() == 1 ? "" : "s");
return result;
}
}
56 changes: 31 additions & 25 deletions comixed-plugins/src/main/java/org/comixed/plugins/model/Plugin.java
Expand Up @@ -20,7 +20,6 @@

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import lombok.extern.log4j.Log4j2;
Expand Down Expand Up @@ -50,12 +49,7 @@ public class Plugin {

@Autowired private PluginInterpreterLoader interpreterLoader;

private Map<String, byte[]> entries = new HashMap<>();
private String language;
private String name;
private String version;
private String description;
private String author;
private PluginDescriptor descriptor;

/**
* Executes the plugin.
Expand All @@ -65,27 +59,27 @@ public class Plugin {
void execute() throws PluginException {}

public String getLanguage() {
return language;
return this.descriptor.getLanguage();
}

public String getName() {
return name;
return this.descriptor.getName();
}

public String getVersion() {
return version;
return this.descriptor.getVersion();
}

public String getDescription() {
return description;
return this.descriptor.getDescription();
}

public String getAuthor() {
return author;
return this.descriptor.getAuthor();
}

public Map<String, byte[]> getEntries() {
return entries;
return this.descriptor.getEntries();
}

/**
Expand All @@ -95,8 +89,10 @@ public Map<String, byte[]> getEntries() {
* @throws PluginException if an error occurs
*/
public void setEntries(Map<String, byte[]> entries) throws PluginException {
this.entries = entries;
byte[] content = this.entries.get(MANIFEST_FILENAME);
this.descriptor = new PluginDescriptor(this);
this.descriptor.setEntries(entries);

byte[] content = entries.get(MANIFEST_FILENAME);
if (content == null || content.length == 0)
throw new PluginException("Missing plugin manifest file");
log.debug("loading plugin manifest file");
Expand All @@ -106,16 +102,26 @@ public void setEntries(Map<String, byte[]> entries) throws PluginException {
} catch (IOException error) {
throw new PluginException("could not read plugin manifest file", error);
}
this.language = (String) properties.get(PLUGIN_LANGUAGE);
if (StringUtils.isEmpty(this.language))
this.descriptor.setLanguage((String) properties.get(PLUGIN_LANGUAGE));
if (StringUtils.isEmpty(this.descriptor.getLanguage()))
throw new PluginException("plugin must declare a language");
if (!this.interpreterLoader.hasLanguage(this.language))
throw new PluginException("plugin language not supported: " + this.language);
this.name = (String) properties.get(PLUGIN_NAME);
if (StringUtils.isEmpty(this.name)) throw new PluginException("plugin must have a name");
this.version = (String) properties.getOrDefault(PLUGIN_VERSION, "unknown");
this.description = (String) properties.getOrDefault(PLUGIN_DESCRIPTION, "No description");
this.author = (String) properties.getOrDefault(PLUGIN_AUTHOR, "anonymous");
this.entries = entries;
if (!this.interpreterLoader.hasLanguage(this.descriptor.getLanguage()))
throw new PluginException("plugin language not supported: " + this.descriptor.getLanguage());
this.descriptor.setName((String) properties.get(PLUGIN_NAME));
if (StringUtils.isEmpty(this.descriptor.getName()))
throw new PluginException("plugin must have a name");
this.descriptor.setVersion((String) properties.getOrDefault(PLUGIN_VERSION, "unknown"));
this.descriptor.setDescription(
(String) properties.getOrDefault(PLUGIN_DESCRIPTION, "No description"));
this.descriptor.setAuthor((String) properties.getOrDefault(PLUGIN_AUTHOR, "anonymous"));
}

/**
* Creates a {@link PluginDescriptor} for the plugin.
*
* @return the descriptor
*/
public PluginDescriptor getDescriptor() {
return this.descriptor;
}
}
@@ -0,0 +1,71 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2020, The ComiXed Project
*
* 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, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see <http://www.gnu.org/licenses>
*/

package org.comixed.plugins.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonView;
import java.util.HashMap;
import java.util.Map;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.comixed.views.View;

/**
* <code>PluginDescriptor</code> holds the details for a plugin.
*
* @author Darryl L. Pierce
*/
@RequiredArgsConstructor
public class PluginDescriptor {
@NonNull @Getter private Plugin plugin;

@Getter
@Setter
@JsonProperty("name")
@JsonView(View.PluginList.class)
private String name;

@Getter
@Setter
@JsonProperty("language")
@JsonView(View.PluginList.class)
private String language;

@Getter
@Setter
@JsonProperty("version")
@JsonView(View.PluginList.class)
private String version;

@Getter
@Setter
@JsonProperty("author")
@JsonView(View.PluginList.class)
private String author;

@Getter
@Setter
@JsonProperty("description")
@JsonView(View.PluginList.class)
private String description;

@Getter @Setter private Map<String, byte[]> entries = new HashMap<>();
}
Expand Up @@ -22,9 +22,9 @@

import java.io.File;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import org.comixed.plugins.model.Plugin;
import org.comixed.plugins.model.PluginDescriptor;
import org.comixed.utils.FileTypeIdentifier;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand All @@ -36,21 +36,14 @@
public class PluginManagerTest {
private static final String TEST_UNDEFINED_PLUGIN_NAME = "plugin-doesnt-exist";
private static final String TEST_PLUGIN_NAME = "test-plugin";
private static final Map<String, byte[]> TEST_PLUGIN_ENTRIES = new HashMap<>();
private static final String TEST_EXAMPLE_PLUGIN_FILE = "src/test/resources/example-plugin.cxp";

static {
TEST_PLUGIN_ENTRIES.put(
Plugin.MANIFEST_FILENAME,
("# this is a test manifest\n" + (Plugin.PLUGIN_NAME + ": " + TEST_PLUGIN_NAME + "\n"))
.getBytes());
}

@InjectMocks private PluginManager pluginManager;
@Mock private ObjectFactory<Plugin> pluginObjectFactory;
@Mock private Plugin plugin;
@Mock private FileTypeIdentifier fileTypeIdentifier;
@Captor private ArgumentCaptor<InputStream> inputStreamArgumentCaptor;
@Mock private PluginDescriptor pluginDescriptor;

@Test(expected = PluginException.class)
public void testLoadPluginNotDefined() throws PluginException {
Expand All @@ -59,18 +52,14 @@ public void testLoadPluginNotDefined() throws PluginException {

@Test
public void testLoadPlugin() throws PluginException {
pluginManager.plugins.put(TEST_PLUGIN_NAME, TEST_PLUGIN_ENTRIES);
pluginManager.plugins.put(TEST_PLUGIN_NAME, pluginDescriptor);

Mockito.when(pluginObjectFactory.getObject()).thenReturn(plugin);
Mockito.doNothing().when(plugin).setEntries(Mockito.anyMap());
Mockito.when(pluginDescriptor.getPlugin()).thenReturn(plugin);

Plugin result = pluginManager.loadPlugin(TEST_PLUGIN_NAME);

assertNotNull(result);
assertSame(plugin, result);

Mockito.verify(pluginObjectFactory, Mockito.times(1)).getObject();
Mockito.verify(plugin, Mockito.times(1)).setEntries(TEST_PLUGIN_ENTRIES);
}

@Test(expected = PluginException.class)
Expand Down Expand Up @@ -114,4 +103,16 @@ public void testLoadPlugins() throws PluginException {
Mockito.verify(fileTypeIdentifier, Mockito.times(1))
.subtypeFor(inputStreamArgumentCaptor.getValue());
}

@Test
public void testGetPluginList() {
for (int index = 0; index < 25; index++)
pluginManager.plugins.put(TEST_PLUGIN_NAME + index, pluginDescriptor);

final List<PluginDescriptor> result = pluginManager.getPluginList();

assertNotNull(result);
assertFalse(result.isEmpty());
assertEquals(25, result.size());
}
}
@@ -0,0 +1,50 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2020, The ComiXed Project
*
* 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, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see <http://www.gnu.org/licenses>
*/

package org.comixed.controller.core;

import com.fasterxml.jackson.annotation.JsonView;
import java.util.List;
import lombok.extern.log4j.Log4j2;
import org.comixed.plugins.PluginManager;
import org.comixed.plugins.model.PluginDescriptor;
import org.comixed.views.View;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* <code>PLuginsController</code> accepts requests relating to the plugin subsystem.
*
* @author Darryl L. Pierce
*/
@RestController
@RequestMapping("/api")
@Log4j2
public class PluginsController {
@Autowired private PluginManager pluginManager;

@GetMapping(value = "/plugins", produces = MediaType.APPLICATION_JSON_VALUE)
@JsonView(View.PluginList.class)
public List<PluginDescriptor> getList() {
log.info("Fetching the list of plugins");
return this.pluginManager.getPluginList();
}
}

0 comments on commit 59d51af

Please sign in to comment.