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.helper;
17  
18  import java.io.IOException;
19  import java.nio.file.FileVisitOption;
20  import java.nio.file.Files;
21  import java.nio.file.Path;
22  import java.nio.file.StandardCopyOption;
23  import java.util.Comparator;
24  import java.util.stream.Stream;
25  import java.util.zip.ZipEntry;
26  import java.util.zip.ZipInputStream;
27  
28  import org.apache.logging.log4j.LogManager;
29  import org.apache.logging.log4j.Logger;
30  import org.codelibs.core.lang.StringUtil;
31  import org.codelibs.core.stream.StreamUtil;
32  import org.codelibs.fess.exception.ThemeException;
33  import org.codelibs.fess.helper.PluginHelper.Artifact;
34  import org.codelibs.fess.helper.PluginHelper.ArtifactType;
35  import org.codelibs.fess.util.ResourceUtil;
36  
37  /**
38   * Helper class for managing theme installation and uninstallation.
39   * Handles the extraction and deployment of theme files from JAR artifacts.
40   */
41  public class ThemeHelper {
42      private static final Logger logger = LogManager.getLogger(ThemeHelper.class);
43  
44      /**
45       * Default constructor for ThemeHelper.
46       */
47      public ThemeHelper() {
48          // Default constructor
49      }
50  
51      /**
52       * Installs a theme from the given artifact.
53       * Extracts theme files from the JAR and deploys them to appropriate directories.
54       *
55       * @param artifact the theme artifact to install
56       * @throws ThemeException if installation fails
57       */
58      public void install(final Artifact artifact) {
59          final Path jarPath = getJarFile(artifact);
60          final String themeName = getThemeName(artifact);
61          if (logger.isDebugEnabled()) {
62              logger.debug("Theme: name={}", themeName);
63          }
64          try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(jarPath))) {
65              ZipEntry entry;
66              while ((entry = zis.getNextEntry()) != null) {
67                  if (!entry.isDirectory()) {
68                      final String[] names = StreamUtil.split(entry.getName(), "/")
69                              .get(stream -> stream.filter(s -> !"..".equals(s)).toArray(n -> new String[n]));
70                      if (names.length < 2) {
71                          continue;
72                      }
73                      if (logger.isDebugEnabled()) {
74                          logger.debug("Loading entry: name={}", entry.getName());
75                      }
76                      if ("view".equals(names[0])) {
77                          names[0] = themeName;
78                          final Path path = ResourceUtil.getViewTemplatePath(names);
79                          Files.createDirectories(path.getParent());
80                          Files.copy(zis, path, StandardCopyOption.REPLACE_EXISTING);
81                      } else if ("css".equals(names[0])) {
82                          names[0] = themeName;
83                          final Path path = ResourceUtil.getCssPath(names);
84                          Files.createDirectories(path.getParent());
85                          Files.copy(zis, path, StandardCopyOption.REPLACE_EXISTING);
86                      } else if ("js".equals(names[0])) {
87                          names[0] = themeName;
88                          final Path path = ResourceUtil.getJavaScriptPath(names);
89                          Files.createDirectories(path.getParent());
90                          Files.copy(zis, path, StandardCopyOption.REPLACE_EXISTING);
91                      } else if ("images".equals(names[0])) {
92                          names[0] = themeName;
93                          final Path path = ResourceUtil.getImagePath(names);
94                          Files.createDirectories(path.getParent());
95                          Files.copy(zis, path, StandardCopyOption.REPLACE_EXISTING);
96                      }
97                  }
98              }
99          } catch (final IOException e) {
100             throw new ThemeException("Failed to install " + artifact, e);
101         }
102     }
103 
104     /**
105      * Uninstalls a theme by removing all its associated files and directories.
106      *
107      * @param artifact the theme artifact to uninstall
108      */
109     public void uninstall(final Artifact artifact) {
110         final String themeName = getThemeName(artifact);
111 
112         final Path viewPath = ResourceUtil.getViewTemplatePath(themeName);
113         closeQuietly(viewPath);
114         final Path imagePath = ResourceUtil.getImagePath(themeName);
115         closeQuietly(imagePath);
116         final Path cssPath = ResourceUtil.getCssPath(themeName);
117         closeQuietly(cssPath);
118         final Path jsPath = ResourceUtil.getJavaScriptPath(themeName);
119         closeQuietly(jsPath);
120     }
121 
122     /**
123      * Extracts the theme name from the artifact name.
124      *
125      * @param artifact the theme artifact
126      * @return the theme name
127      * @throws ThemeException if theme name cannot be determined
128      */
129     protected String getThemeName(final Artifact artifact) {
130         final String themeName = artifact.getName().substring(ArtifactType.THEME.getId().length() + 1);
131         if (StringUtil.isBlank(themeName)) {
132             throw new ThemeException("Theme name is empty: " + artifact);
133         }
134         return themeName;
135     }
136 
137     /**
138      * Recursively deletes a directory and all its contents.
139      * Does not throw exceptions, only logs warnings if deletion fails.
140      *
141      * @param dir the directory to delete
142      */
143     protected void closeQuietly(final Path dir) {
144         if (Files.notExists(dir)) {
145             if (logger.isDebugEnabled()) {
146                 logger.debug("Path does not exist: path={}", dir);
147             }
148             return;
149         }
150         try (Stream<Path> walk = Files.walk(dir, FileVisitOption.FOLLOW_LINKS)) {
151             walk.sorted(Comparator.reverseOrder()).forEach(f -> {
152                 if (logger.isDebugEnabled()) {
153                     logger.debug("Deleting: path={}", f);
154                 }
155                 try {
156                     Files.delete(f);
157                 } catch (final IOException e) {
158                     logger.warn("Failed to delete: path={}", f, e);
159                 }
160             });
161             Files.deleteIfExists(dir);
162         } catch (final IOException e) {
163             logger.warn("Failed to delete: path={}", dir, e);
164         }
165     }
166 
167     /**
168      * Gets the JAR file path for the given artifact.
169      *
170      * @param artifact the theme artifact
171      * @return the path to the JAR file
172      * @throws ThemeException if the JAR file does not exist
173      */
174     protected Path getJarFile(final Artifact artifact) {
175         final Path jarPath = ResourceUtil.getPluginPath(artifact.getFileName());
176         if (!Files.exists(jarPath)) {
177             throw new ThemeException(artifact.getFileName() + " does not exist.");
178         }
179         return jarPath;
180     }
181 
182 }