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.dict;
17  
18  import java.io.File;
19  import java.io.IOException;
20  import java.text.SimpleDateFormat;
21  import java.util.ArrayList;
22  import java.util.Date;
23  import java.util.List;
24  import java.util.Map;
25  
26  import org.apache.logging.log4j.LogManager;
27  import org.apache.logging.log4j.Logger;
28  import org.codelibs.core.io.FileUtil;
29  import org.codelibs.curl.CurlResponse;
30  import org.codelibs.fess.Constants;
31  import org.codelibs.fess.util.ComponentUtil;
32  import org.codelibs.opensearch.runner.net.OpenSearchCurl;
33  import org.dbflute.optional.OptionalEntity;
34  
35  import jakarta.annotation.PostConstruct;
36  
37  /**
38   * Manager class for handling dictionary files in the Fess search system.
39   * This class provides functionality to retrieve, store, and manage various
40   * dictionary files such as synonyms, kuromoji, protwords, and stopwords.
41   * It coordinates with DictionaryCreator instances to handle different
42   * dictionary types and manages file synchronization through ConfigSync.
43   *
44   */
45  public class DictionaryManager {
46      private static final Logger logger = LogManager.getLogger(DictionaryManager.class);
47  
48      /** List of dictionary creators for handling different dictionary types */
49      protected List<DictionaryCreator> creatorList = new ArrayList<>();
50  
51      /**
52       * Default constructor for DictionaryManager.
53       * Creates a new dictionary manager with an empty creator list.
54       */
55      public DictionaryManager() {
56          // Default constructor
57      }
58  
59      /**
60       * Initializes the dictionary manager after construction.
61       * Sets up the relationship between this manager and all registered creators.
62       */
63      @PostConstruct
64      public void init() {
65          if (logger.isDebugEnabled()) {
66              logger.debug("Initializing {}", this.getClass().getSimpleName());
67          }
68          creatorList.forEach(creator -> {
69              creator.setDictionaryManager(this);
70          });
71      }
72  
73      /**
74       * Retrieves all available dictionary files from the ConfigSync storage.
75       * This method queries the ConfigSync API to get file information and
76       * uses registered DictionaryCreator instances to create appropriate
77       * DictionaryFile objects.
78       *
79       * @return an array of dictionary files available in the system
80       * @throws DictionaryException if there's an error accessing the dictionaries
81       */
82      public DictionaryFile<? extends DictionaryItem>[] getDictionaryFiles() {
83          try (CurlResponse response = ComponentUtil.getCurlHelper()
84                  .get("/_configsync/file")
85                  .param("fields", "path,@timestamp")
86                  .param("size", ComponentUtil.getFessConfig().getPageDictionaryMaxFetchSize())
87                  .execute()) {
88              final Map<String, Object> contentMap = response.getContent(OpenSearchCurl.jsonParser());
89              @SuppressWarnings("unchecked")
90              final List<Map<String, Object>> fileList = (List<Map<String, Object>>) contentMap.get("file");
91              return fileList.stream().map(fileMap -> {
92                  try {
93                      final String path = fileMap.get("path").toString();
94                      final Date timestamp =
95                              new SimpleDateFormat(Constants.DATE_FORMAT_ISO_8601_EXTEND_UTC).parse(fileMap.get("@timestamp").toString());
96                      for (final DictionaryCreator creator : creatorList) {
97                          final DictionaryFile<? extends DictionaryItem> file = creator.create(path, timestamp);
98                          if (file != null) {
99                              return file;
100                         }
101                     }
102                 } catch (final Exception e) {
103                     final String filePath = fileMap.get("path") != null ? fileMap.get("path").toString() : "unknown";
104                     final String fileTimestamp = fileMap.get("@timestamp") != null ? fileMap.get("@timestamp").toString() : "unknown";
105                     logger.warn("Failed to load dictionary file: path={}, timestamp={}, error={}", filePath, fileTimestamp, e.getMessage(),
106                             e);
107                 }
108                 return null;
109             }).filter(file -> file != null).toArray(n -> new DictionaryFile<?>[n]);
110         } catch (final IOException e) {
111             throw new DictionaryException("Failed to access dictionaries", e);
112         }
113     }
114 
115     /**
116      * Retrieves a specific dictionary file by its ID.
117      *
118      * @param id the unique identifier of the dictionary file to retrieve
119      * @return an OptionalEntity containing the dictionary file if found, empty otherwise
120      */
121     public OptionalEntity<DictionaryFile<? extends DictionaryItem>> getDictionaryFile(final String id) {
122         for (final DictionaryFile<? extends DictionaryItem> dictFile : getDictionaryFiles()) {
123             if (dictFile.getId().equals(id)) {
124                 return OptionalEntity.of(dictFile);
125             }
126         }
127         return OptionalEntity.empty();
128     }
129 
130     /**
131      * Stores or updates a dictionary file in the ConfigSync storage.
132      * This method checks for concurrent modifications by comparing timestamps
133      * and uploads the file content to the ConfigSync API.
134      *
135      * @param dictFile the dictionary file metadata to store
136      * @param file the actual file containing the dictionary content
137      * @throws DictionaryException if the file was updated by another process,
138      *         if the file doesn't exist, or if there's an error during storage
139      */
140     public void store(final DictionaryFile<? extends DictionaryItem> dictFile, final File file) {
141         getDictionaryFile(dictFile.getId()).ifPresent(currentFile -> {
142             if (currentFile.getTimestamp().getTime() > dictFile.getTimestamp().getTime()) {
143                 throw new DictionaryException(dictFile.getPath() + " was updated.");
144             }
145 
146             // TODO use stream
147             try (CurlResponse response = ComponentUtil.getCurlHelper()
148                     .post("/_configsync/file")
149                     .param("path", dictFile.getPath())
150                     .body(FileUtil.readUTF8(file))
151                     .execute()) {
152                 final Map<String, Object> contentMap = response.getContent(OpenSearchCurl.jsonParser());
153                 if (!Constants.TRUE.equalsIgnoreCase(contentMap.get("acknowledged").toString())) {
154                     throw new DictionaryException("Failed to update " + dictFile.getPath());
155                 }
156             } catch (final IOException e) {
157                 throw new DictionaryException("Failed to update " + dictFile.getPath(), e);
158             }
159 
160         }).orElse(() -> {
161             throw new DictionaryException(dictFile.getPath() + " does not exist.");
162         });
163     }
164 
165     /**
166      * Gets the HTTP response containing the content of a dictionary file.
167      * This method retrieves the raw file content from ConfigSync storage.
168      *
169      * @param dictFile the dictionary file to retrieve content for
170      * @return a CurlResponse containing the file content
171      */
172     public CurlResponse getContentResponse(final DictionaryFile<? extends DictionaryItem> dictFile) {
173         return ComponentUtil.getCurlHelper().get("/_configsync/file").param("path", dictFile.getPath()).execute();
174     }
175 
176     /**
177      * Adds a new dictionary creator to this manager.
178      * Dictionary creators are responsible for creating specific types
179      * of dictionary files based on file paths and timestamps.
180      *
181      * @param creator the dictionary creator to add
182      */
183     public void addCreator(final DictionaryCreator creator) {
184         creatorList.add(creator);
185     }
186 
187 }