View Javadoc
1   /*
2    * Copyright 2012-2025 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.app.web.admin.plugin;
17  
18  import java.io.File;
19  import java.io.FileOutputStream;
20  import java.io.InputStream;
21  import java.io.OutputStream;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.stream.Collectors;
28  
29  import org.apache.logging.log4j.LogManager;
30  import org.apache.logging.log4j.Logger;
31  import org.codelibs.core.io.CopyUtil;
32  import org.codelibs.fess.annotation.Secured;
33  import org.codelibs.fess.app.web.base.FessAdminAction;
34  import org.codelibs.fess.helper.PluginHelper;
35  import org.codelibs.fess.helper.PluginHelper.Artifact;
36  import org.codelibs.fess.helper.PluginHelper.ArtifactType;
37  import org.codelibs.fess.util.ComponentUtil;
38  import org.codelibs.fess.util.RenderDataUtil;
39  import org.lastaflute.web.Execute;
40  import org.lastaflute.web.response.HtmlResponse;
41  import org.lastaflute.web.ruts.process.ActionRuntime;
42  import org.lastaflute.web.validation.exception.ValidationErrorException;
43  
44  /**
45   * Admin action for Plugin management.
46   * This class provides functionality for installing, deleting, and managing plugins in the Fess system.
47   *
48   */
49  public class AdminPluginAction extends FessAdminAction {
50  
51      /**
52       * Default constructor.
53       */
54      public AdminPluginAction() {
55          super();
56      }
57  
58      /**
59       * Role identifier for plugin administration.
60       */
61      public static final String ROLE = "admin-plugin";
62  
63      private static final Logger logger = LogManager.getLogger(AdminPluginAction.class);
64  
65      private static final String UPLOAD = "upload";
66  
67      @Override
68      protected void setupHtmlData(final ActionRuntime runtime) {
69          super.setupHtmlData(runtime);
70          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNamePlugin()));
71      }
72  
73      /**
74       * Returns the action role for this controller.
75       *
76       * @return the role identifier for plugin administration
77       */
78      @Override
79      protected String getActionRole() {
80          return ROLE;
81      }
82  
83      /**
84       * Displays the plugin management index page.
85       *
86       * @return HTML response for the index page
87       */
88      @Execute
89      @Secured({ ROLE, ROLE + VIEW })
90      public HtmlResponse index() {
91          saveToken();
92          return asListHtml();
93      }
94  
95      /**
96       * Deletes the specified plugin.
97       *
98       * @param form the delete form containing plugin information
99       * @return HTML response redirecting to the plugin list
100      */
101     @Execute
102     @Secured({ ROLE })
103     public HtmlResponse delete(final DeleteForm form) {
104         validate(form, messages -> {}, () -> asHtml(path_AdminPlugin_AdminPluginJsp));
105         verifyToken(() -> asHtml(path_AdminPlugin_AdminPluginJsp));
106         final Artifact artifact = new Artifact(form.name, form.version, null);
107         deleteArtifact(artifact);
108         saveInfo(messages -> messages.addSuccessDeletePlugin(GLOBAL, artifact.getFileName()));
109         return redirect(getClass());
110     }
111 
112     /**
113      * Installs a plugin from either an uploaded JAR file or from the available artifacts.
114      *
115      * @param form the install form containing plugin installation details
116      * @return HTML response redirecting to the plugin list
117      */
118     @Execute
119     @Secured({ ROLE })
120     public HtmlResponse install(final InstallForm form) {
121         validate(form, messages -> {}, () -> asHtml(path_AdminPlugin_AdminPluginInstallpluginJsp));
122         verifyToken(() -> asHtml(path_AdminPlugin_AdminPluginInstallpluginJsp));
123         try {
124             if (UPLOAD.equals(form.id)) {
125                 if (form.jarFile == null) {
126                     throwValidationError(messages -> messages.addErrorsPluginFileIsNotFound(GLOBAL, form.id), this::asListHtml);
127                 }
128                 if (!form.jarFile.getFileName().endsWith(".jar")) {
129                     throwValidationError(messages -> messages.addErrorsFileIsNotSupported(GLOBAL, form.jarFile.getFileName()),
130                             this::asListHtml);
131                 }
132                 final String filename = form.jarFile.getFileName();
133                 final File tempFile = ComponentUtil.getSystemHelper().createTempFile("tmp-adminplugin-", ".jar");
134                 try (final InputStream is = form.jarFile.getInputStream(); final OutputStream os = new FileOutputStream(tempFile)) {
135                     CopyUtil.copy(is, os);
136                 } catch (final Exception e) {
137                     if (tempFile.exists() && !tempFile.delete()) {
138                         logger.warn("Failed to delete {}.", tempFile.getAbsolutePath());
139                     }
140                     logger.debug("Failed to copy {}", filename, e);
141                     throwValidationError(messages -> messages.addErrorsFailedToInstallPlugin(GLOBAL, filename), this::asListHtml);
142                 }
143                 new Thread(() -> {
144                     try {
145                         final PluginHelper pluginHelper = ComponentUtil.getPluginHelper();
146                         final Artifact artifact =
147                                 pluginHelper.getArtifactFromFileName(ArtifactType.UNKNOWN, filename, tempFile.getAbsolutePath());
148                         pluginHelper.installArtifact(artifact);
149                     } catch (final Exception e) {
150                         logger.warn("Failed to install {}", filename, e);
151                     } finally {
152                         if (tempFile.exists() && !tempFile.delete()) {
153                             logger.warn("Failed to delete {}.", tempFile.getAbsolutePath());
154                         }
155                     }
156                 }).start();
157                 saveInfo(messages -> messages.addSuccessInstallPlugin(GLOBAL, form.jarFile.getFileName()));
158             } else {
159                 final Artifact artifact = getArtifactFromInstallForm(form);
160                 if (artifact == null) {
161                     throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.id), this::asListHtml);
162                 }
163                 installArtifact(artifact);
164                 saveInfo(messages -> messages.addSuccessInstallPlugin(GLOBAL, artifact.getFileName()));
165             }
166         } catch (final ValidationErrorException e) {
167             throw e;
168         } catch (final Exception e) {
169             throwValidationError(messages -> messages.addErrorsFailedToInstallPlugin(GLOBAL, form.id), this::asListHtml);
170         }
171         return redirect(getClass());
172     }
173 
174     /**
175      * Displays the plugin installation page with available plugins.
176      *
177      * @return HTML response for the plugin installation page
178      */
179     @Execute
180     @Secured({ ROLE })
181     public HtmlResponse installplugin() {
182         saveToken();
183         return asHtml(path_AdminPlugin_AdminPluginInstallpluginJsp).renderWith(data -> {
184             final List<Map<String, String>> result = new ArrayList<>();
185             final Map<String, String> map = new HashMap<>();
186             map.put("id", UPLOAD);
187             map.put("name", "");
188             map.put("version", "");
189             result.add(map);
190             try {
191                 result.addAll(getAllAvailableArtifacts());
192             } catch (final Exception e) {
193                 saveError(messages -> messages.addErrorsFailedToFindPlugins(GLOBAL));
194                 logger.warn("Failed to access a plugin repository.", e);
195             }
196             RenderDataUtil.register(data, "availableArtifactItems", result);
197         }).useForm(InstallForm.class, op -> op.setup(form -> {}));
198     }
199 
200     private HtmlResponse asListHtml() {
201         return asHtml(path_AdminPlugin_AdminPluginJsp)
202                 .renderWith(data -> data.register("installedArtifactItems", getAllInstalledArtifacts()))
203                 .useForm(DeleteForm.class);
204     }
205 
206     /**
207      * Retrieves all available artifacts from all plugin types.
208      *
209      * @return list of maps containing artifact information
210      */
211     public static List<Map<String, String>> getAllAvailableArtifacts() {
212         final PluginHelper pluginHelper = ComponentUtil.getPluginHelper();
213         final List<Map<String, String>> result = new ArrayList<>();
214         for (final PluginHelper.ArtifactType artifactType : PluginHelper.ArtifactType.values()) {
215             result.addAll(Arrays.stream(pluginHelper.getAvailableArtifacts(artifactType))
216                     .map(AdminPluginAction::beanToMap)
217                     .collect(Collectors.toList()));
218         }
219         return result;
220     }
221 
222     /**
223      * Retrieves all installed artifacts from all plugin types.
224      *
225      * @return list of maps containing installed artifact information
226      */
227     public static List<Map<String, String>> getAllInstalledArtifacts() {
228         final PluginHelper pluginHelper = ComponentUtil.getPluginHelper();
229         final List<Map<String, String>> result = new ArrayList<>();
230         for (final PluginHelper.ArtifactType artifactType : PluginHelper.ArtifactType.values()) {
231             result.addAll(Arrays.stream(pluginHelper.getInstalledArtifacts(artifactType))
232                     .map(AdminPluginAction::beanToMap)
233                     .collect(Collectors.toList()));
234         }
235         return result;
236     }
237 
238     /**
239      * Converts an Artifact object to a Map representation.
240      *
241      * @param artifact the artifact to convert
242      * @return map containing artifact properties
243      */
244     public static Map<String, String> beanToMap(final Artifact artifact) {
245         final Map<String, String> item = new HashMap<>();
246         item.put("type", artifact.getType().getId());
247         item.put("id", artifact.getName() + ":" + artifact.getVersion());
248         item.put("name", artifact.getName());
249         item.put("version", artifact.getVersion());
250         item.put("url", artifact.getUrl());
251         return item;
252     }
253 
254     private Artifact getArtifactFromInstallForm(final InstallForm form) {
255         final String[] values = form.id.split(":");
256         return ComponentUtil.getPluginHelper().getArtifact(values[0], values[1]);
257     }
258 
259     /**
260      * Installs the specified artifact in a background thread.
261      * Also removes any previously installed versions of the same plugin.
262      *
263      * @param artifact the artifact to install
264      */
265     public static void installArtifact(final Artifact artifact) {
266         new Thread(() -> {
267             final PluginHelper pluginHelper = ComponentUtil.getPluginHelper();
268             final Artifact[] artifacts = pluginHelper.getInstalledArtifacts(artifact.getType());
269             try {
270                 pluginHelper.installArtifact(artifact);
271             } catch (final Exception e) {
272                 logger.warn("Failed to install {}", artifact.getFileName(), e);
273             }
274             for (final Artifact a : artifacts) {
275                 if (a.getName().equals(artifact.getName()) && !a.getVersion().equals(artifact.getVersion())) {
276                     try {
277                         pluginHelper.deleteInstalledArtifact(a);
278                     } catch (final Exception e) {
279                         logger.warn("Failed to delete {}", a.getFileName(), e);
280                     }
281                 }
282             }
283         }).start();
284     }
285 
286     /**
287      * Deletes the specified artifact in a background thread.
288      *
289      * @param artifact the artifact to delete
290      */
291     public static void deleteArtifact(final Artifact artifact) {
292         new Thread(() -> {
293             try {
294                 ComponentUtil.getPluginHelper().deleteInstalledArtifact(artifact);
295             } catch (final Exception e) {
296                 logger.warn("Failed to delete {}", artifact.getFileName(), e);
297             }
298         }).start();
299     }
300 }