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.protwords;
17  
18  import java.io.BufferedReader;
19  import java.io.BufferedWriter;
20  import java.io.Closeable;
21  import java.io.File;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.InputStreamReader;
26  import java.io.OutputStreamWriter;
27  import java.io.Writer;
28  import java.util.ArrayList;
29  import java.util.Collections;
30  import java.util.Date;
31  import java.util.List;
32  
33  import org.codelibs.core.io.CloseableUtil;
34  import org.codelibs.core.lang.StringUtil;
35  import org.codelibs.curl.CurlResponse;
36  import org.codelibs.fess.Constants;
37  import org.codelibs.fess.dict.DictionaryException;
38  import org.codelibs.fess.dict.DictionaryFile;
39  import org.codelibs.fess.util.ComponentUtil;
40  import org.dbflute.optional.OptionalEntity;
41  
42  /**
43   * Dictionary file for protected words.
44   * This class manages the reading, writing, and updating of protected words dictionary files.
45   */
46  public class ProtwordsFile extends DictionaryFile<ProtwordsItem> {
47      private static final String PROTWORDS = "protwords";
48  
49      List<ProtwordsItem> protwordsItemList;
50  
51      /**
52       * Constructor for ProtwordsFile.
53       * @param id the file identifier
54       * @param path the file path
55       * @param timestamp the file timestamp
56       */
57      public ProtwordsFile(final String id, final String path, final Date timestamp) {
58          super(id, path, timestamp);
59      }
60  
61      @Override
62      public String getType() {
63          return PROTWORDS;
64      }
65  
66      @Override
67      public String getPath() {
68          return path;
69      }
70  
71      @Override
72      public synchronized OptionalEntity<ProtwordsItem> get(final long id) {
73          if (protwordsItemList == null) {
74              reload(null);
75          }
76  
77          for (final ProtwordsItem ProtwordsItem : protwordsItemList) {
78              if (id == ProtwordsItem.getId()) {
79                  return OptionalEntity.of(ProtwordsItem);
80              }
81          }
82          return OptionalEntity.empty();
83      }
84  
85      @Override
86      public synchronized PagingList<ProtwordsItem> selectList(final int offset, final int size) {
87          if (protwordsItemList == null) {
88              reload(null);
89          }
90  
91          if (offset >= protwordsItemList.size() || offset < 0) {
92              return new PagingList<>(Collections.<ProtwordsItem> emptyList(), offset, size, protwordsItemList.size());
93          }
94  
95          int toIndex = offset + size;
96          if (toIndex > protwordsItemList.size()) {
97              toIndex = protwordsItemList.size();
98          }
99  
100         return new PagingList<>(protwordsItemList.subList(offset, toIndex), offset, size, protwordsItemList.size());
101     }
102 
103     @Override
104     public synchronized void insert(final ProtwordsItem item) {
105         try (ProtwordsUpdater updater = new ProtwordsUpdater(item)) {
106             reload(updater);
107         }
108     }
109 
110     @Override
111     public synchronized void update(final ProtwordsItem item) {
112         try (ProtwordsUpdater updater = new ProtwordsUpdater(item)) {
113             reload(updater);
114         }
115     }
116 
117     @Override
118     public synchronized void delete(final ProtwordsItem item) {
119         final ProtwordsItem ProtwordsItem = item;
120         ProtwordsItem.setNewInput(StringUtil.EMPTY);
121         try (ProtwordsUpdater updater = new ProtwordsUpdater(item)) {
122             reload(updater);
123         }
124     }
125 
126     /**
127      * Reloads the dictionary file with the specified updater.
128      * @param updater the updater to use for processing items
129      */
130     protected void reload(final ProtwordsUpdater updater) {
131         try (CurlResponse curlResponse = dictionaryManager.getContentResponse(this)) {
132             reload(updater, curlResponse.getContentAsStream());
133         } catch (final IOException e) {
134             throw new DictionaryException("Failed to parse " + path, e);
135         }
136     }
137 
138     /**
139      * Reloads the dictionary file with the specified updater and input stream.
140      * @param updater the updater to use for processing items
141      * @param in the input stream containing the file content
142      */
143     protected void reload(final ProtwordsUpdater updater, final InputStream in) {
144         final List<ProtwordsItem> itemList = new ArrayList<>();
145         try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, Constants.UTF_8))) {
146             long id = 0;
147             String line = null;
148             while ((line = reader.readLine()) != null) {
149                 if (line.length() == 0 || line.charAt(0) == '#') {
150                     if (updater != null) {
151                         updater.write(line);
152                     }
153                     continue; // ignore empty lines and comments
154                 }
155 
156                 final String inputStrings = line;
157                 final String input = unescape(inputStrings);
158 
159                 if (input.length() > 0) {
160                     id++;
161                     final ProtwordsItem item = new ProtwordsItem(id, input);
162                     if (updater != null) {
163                         final ProtwordsItem newItem = updater.write(item);
164                         if (newItem != null) {
165                             itemList.add(newItem);
166                         } else {
167                             id--;
168                         }
169                     } else {
170                         itemList.add(item);
171                     }
172                 }
173             }
174             if (updater != null) {
175                 final ProtwordsItem item = updater.commit();
176                 if (item != null) {
177                     itemList.add(item);
178                 }
179             }
180             protwordsItemList = itemList;
181         } catch (final IOException e) {
182             throw new DictionaryException("Failed to parse " + path, e);
183         }
184     }
185 
186     private String unescape(final String s) {
187         if (s.indexOf('\\') >= 0) {
188             final StringBuilder sb = new StringBuilder();
189             for (int i = 0; i < s.length(); i++) {
190                 final char ch = s.charAt(i);
191                 if (ch == '\\' && i < s.length() - 1) {
192                     i++;
193                     sb.append(s.charAt(i));
194                 } else {
195                     sb.append(ch);
196                 }
197             }
198             return sb.toString();
199         }
200         return s;
201     }
202 
203     /**
204      * Gets the simple name of this dictionary file.
205      * @return the simple name of the file
206      */
207     public String getSimpleName() {
208         return new File(path).getName();
209     }
210 
211     /**
212      * Updates the dictionary file with content from the input stream.
213      * @param in the input stream containing the new content
214      * @throws IOException if an I/O error occurs
215      */
216     public synchronized void update(final InputStream in) throws IOException {
217         try (ProtwordsUpdater updater = new ProtwordsUpdater(null)) {
218             reload(updater, in);
219         }
220     }
221 
222     @Override
223     public String toString() {
224         return "ProtwordsFile [path=" + path + ", protwordsItemList=" + protwordsItemList + ", id=" + id + "]";
225     }
226 
227     /**
228      * Updater class for processing protwords items during dictionary updates.
229      * This class handles the writing and committing of changes to the dictionary file.
230      */
231     protected class ProtwordsUpdater implements Closeable {
232 
233         /** Flag indicating if the update should be committed */
234         protected boolean isCommit = false;
235 
236         /** Temporary file for storing updates */
237         protected File newFile;
238 
239         /** Writer for writing to the temporary file */
240         protected Writer writer;
241 
242         /** The item being updated */
243         protected ProtwordsItem item;
244 
245         /**
246          * Constructor for ProtwordsUpdater.
247          * @param newItem the item to be updated
248          */
249         protected ProtwordsUpdater(final ProtwordsItem newItem) {
250             FileOutputStream fos = null;
251             try {
252                 newFile = ComponentUtil.getSystemHelper().createTempFile(PROTWORDS, ".txt");
253                 fos = new FileOutputStream(newFile);
254                 writer = new BufferedWriter(new OutputStreamWriter(fos, Constants.UTF_8));
255                 fos = null; // Successfully wrapped, no need to close explicitly
256             } catch (final Exception e) {
257                 if (fos != null) {
258                     try {
259                         fos.close();
260                     } catch (final IOException ioe) {
261                         // Ignore close exception
262                     }
263                 }
264                 if (newFile != null) {
265                     newFile.delete();
266                 }
267                 throw new DictionaryException("Failed to write a userDict file.", e);
268             }
269             item = newItem;
270         }
271 
272         /**
273          * Writes a protwords item to the temporary file.
274          * @param oldItem the item to write
275          * @return the written item or null if deleted
276          */
277         public ProtwordsItem write(final ProtwordsItem oldItem) {
278             try {
279                 if (item == null || item.getId() != oldItem.getId() || !item.isUpdated()) {
280                     writer.write(oldItem.toLineString());
281                     writer.write(Constants.LINE_SEPARATOR);
282                     return oldItem;
283                 }
284                 if (!item.equals(oldItem)) {
285                     throw new DictionaryException("Protwords file was updated: old=" + oldItem + " : new=" + item);
286                 }
287                 try {
288                     if (!item.isDeleted()) {
289                         // update
290                         writer.write(item.toLineString());
291                         writer.write(Constants.LINE_SEPARATOR);
292                         return new ProtwordsItem(item.getId(), item.getNewInput());
293                     }
294                     return null;
295                 } finally {
296                     item.setNewInput(null);
297                 }
298             } catch (final IOException e) {
299                 throw new DictionaryException("Failed to write: " + oldItem + " -> " + item, e);
300             }
301         }
302 
303         /**
304          * Writes a string line to the temporary file.
305          * @param line the line to write
306          */
307         public void write(final String line) {
308             try {
309                 writer.write(line);
310                 writer.write(Constants.LINE_SEPARATOR);
311             } catch (final IOException e) {
312                 throw new DictionaryException("Failed to write: " + line, e);
313             }
314         }
315 
316         /**
317          * Commits the current item to the temporary file.
318          * @return the committed item or null if no item to commit
319          */
320         public ProtwordsItem commit() {
321             isCommit = true;
322             if (item != null && item.isUpdated()) {
323                 try {
324                     writer.write(item.toLineString());
325                     writer.write(Constants.LINE_SEPARATOR);
326                     return item;
327                 } catch (final IOException e) {
328                     throw new DictionaryException("Failed to write: " + item, e);
329                 }
330             }
331             return null;
332         }
333 
334         @Override
335         public void close() {
336             try {
337                 writer.flush();
338             } catch (final IOException e) {
339                 // ignore
340             }
341             CloseableUtil.closeQuietly(writer);
342 
343             if (isCommit) {
344                 try {
345                     dictionaryManager.store(ProtwordsFile.this, newFile);
346                 } finally {
347                     newFile.delete();
348                 }
349             } else {
350                 newFile.delete();
351             }
352         }
353     }
354 }