View Javadoc
1   /*
2    * Copyright 2012-2021 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.synonym;
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  public class SynonymFile extends DictionaryFile<SynonymItem> {
43      private static final String SYNONYM = "synonym";
44  
45      List<SynonymItem> synonymItemList;
46  
47      public SynonymFile(final String id, final String path, final Date timestamp) {
48          super(id, path, timestamp);
49      }
50  
51      @Override
52      public String getType() {
53          return SYNONYM;
54      }
55  
56      @Override
57      public String getPath() {
58          return path;
59      }
60  
61      @Override
62      public synchronized OptionalEntity<SynonymItem> get(final long id) {
63          if (synonymItemList == null) {
64              reload(null);
65          }
66  
67          for (final SynonymItem synonymItem : synonymItemList) {
68              if (id == synonymItem.getId()) {
69                  return OptionalEntity.of(synonymItem);
70              }
71          }
72          return OptionalEntity.empty();
73      }
74  
75      @Override
76      public synchronized PagingList<SynonymItem> selectList(final int offset, final int size) {
77          if (synonymItemList == null) {
78              reload(null);
79          }
80  
81          if (offset >= synonymItemList.size() || offset < 0) {
82              return new PagingList<>(Collections.<SynonymItem> emptyList(), offset, size, synonymItemList.size());
83          }
84  
85          int toIndex = offset + size;
86          if (toIndex > synonymItemList.size()) {
87              toIndex = synonymItemList.size();
88          }
89  
90          return new PagingList<>(synonymItemList.subList(offset, toIndex), offset, size, synonymItemList.size());
91      }
92  
93      @Override
94      public synchronized void insert(final SynonymItem item) {
95          try (SynonymUpdater updater = new SynonymUpdater(item)) {
96              reload(updater);
97          }
98      }
99  
100     @Override
101     public synchronized void update(final SynonymItem item) {
102         try (SynonymUpdater updater = new SynonymUpdater(item)) {
103             reload(updater);
104         }
105     }
106 
107     @Override
108     public synchronized void delete(final SynonymItem item) {
109         final SynonymItem synonymItem = item;
110         synonymItem.setNewInputs(StringUtil.EMPTY_STRINGS);
111         synonymItem.setNewOutputs(StringUtil.EMPTY_STRINGS);
112         try (SynonymUpdater updater = new SynonymUpdater(item)) {
113             reload(updater);
114         }
115     }
116 
117     protected void reload(final SynonymUpdater updater) {
118         try (CurlResponse curlResponse = dictionaryManager.getContentResponse(this)) {
119             reload(updater, curlResponse.getContentAsStream());
120         } catch (final IOException e) {
121             throw new DictionaryException("Failed to parse " + path, e);
122         }
123     }
124 
125     protected void reload(final SynonymUpdater updater, final InputStream in) {
126         final List<SynonymItem> itemList = new ArrayList<>();
127         try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, Constants.UTF_8))) {
128             long id = 0;
129             String line = null;
130             while ((line = reader.readLine()) != null) {
131                 if (line.length() == 0 || line.charAt(0) == '#') {
132                     if (updater != null) {
133                         updater.write(line);
134                     }
135                     continue; // ignore empty lines and comments
136                 }
137 
138                 String[] inputs;
139                 String[] outputs;
140 
141                 final List<String> sides = split(line, "=>");
142                 if (sides.size() > 1) { // explicit mapping
143                     if (sides.size() != 2) {
144                         throw new DictionaryException("more than one explicit mapping specified on the same line");
145                     }
146                     final List<String> inputStrings = split(sides.get(0), ",");
147                     inputs = new String[inputStrings.size()];
148                     for (int i = 0; i < inputs.length; i++) {
149                         inputs[i] = unescape(inputStrings.get(i)).trim();
150                     }
151 
152                     final List<String> outputStrings = split(sides.get(1), ",");
153                     outputs = new String[outputStrings.size()];
154                     for (int i = 0; i < outputs.length; i++) {
155                         outputs[i] = unescape(outputStrings.get(i)).trim();
156                     }
157 
158                     if (inputs.length > 0 && outputs.length > 0) {
159                         id++;
160                         final SynonymItem item = new SynonymItem(id, inputs, outputs);
161                         if (updater != null) {
162                             final SynonymItem newItem = updater.write(item);
163                             if (newItem != null) {
164                                 itemList.add(newItem);
165                             } else {
166                                 id--;
167                             }
168                         } else {
169                             itemList.add(item);
170                         }
171                     }
172                 } else {
173                     final List<String> inputStrings = split(line, ",");
174                     inputs = new String[inputStrings.size()];
175                     for (int i = 0; i < inputs.length; i++) {
176                         inputs[i] = unescape(inputStrings.get(i)).trim();
177                     }
178 
179                     if (inputs.length > 0) {
180                         id++;
181                         final SynonymItem item = new SynonymItem(id, inputs, inputs);
182                         if (updater != null) {
183                             final SynonymItem newItem = updater.write(item);
184                             if (newItem != null) {
185                                 itemList.add(newItem);
186                             } else {
187                                 id--;
188                             }
189                         } else {
190                             itemList.add(item);
191                         }
192                     }
193                 }
194             }
195             if (updater != null) {
196                 final SynonymItem item = updater.commit();
197                 if (item != null) {
198                     itemList.add(item);
199                 }
200             }
201             synonymItemList = itemList;
202         } catch (final IOException e) {
203             throw new DictionaryException("Failed to parse " + path, e);
204         }
205     }
206 
207     private static List<String> split(final String s, final String separator) {
208         final List<String> list = new ArrayList<>(2);
209         StringBuilder sb = new StringBuilder();
210         int pos = 0;
211         final int end = s.length();
212         while (pos < end) {
213             if (s.startsWith(separator, pos)) {
214                 if (sb.length() > 0) {
215                     list.add(sb.toString());
216                     sb = new StringBuilder();
217                 }
218                 pos += separator.length();
219                 continue;
220             }
221 
222             char ch = s.charAt(pos);
223             pos++;
224             if (ch == '\\') {
225                 sb.append(ch);
226                 if (pos >= end) {
227                     break; // ERROR, or let it go?
228                 }
229                 ch = s.charAt(pos);
230                 pos++;
231             }
232 
233             sb.append(ch);
234         }
235 
236         if (sb.length() > 0) {
237             list.add(sb.toString());
238         }
239 
240         return list;
241     }
242 
243     private String unescape(final String s) {
244         if (s.indexOf('\\') >= 0) {
245             final StringBuilder sb = new StringBuilder();
246             for (int i = 0; i < s.length(); i++) {
247                 final char ch = s.charAt(i);
248                 if (ch == '\\' && i < s.length() - 1) {
249                     i++;
250                     sb.append(s.charAt(i));
251                 } else {
252                     sb.append(ch);
253                 }
254             }
255             return sb.toString();
256         }
257         return s;
258     }
259 
260     public String getSimpleName() {
261         return new File(path).getName();
262     }
263 
264     public synchronized void update(final InputStream in) throws IOException {
265         try (SynonymUpdater updater = new SynonymUpdater(null)) {
266             reload(updater, in);
267         }
268     }
269 
270     @Override
271     public String toString() {
272         return "SynonymFile [path=" + path + ", synonymItemList=" + synonymItemList + ", id=" + id + "]";
273     }
274 
275     protected class SynonymUpdater implements Closeable {
276 
277         protected boolean isCommit = false;
278 
279         protected File newFile;
280 
281         protected Writer writer;
282 
283         protected SynonymItem item;
284 
285         protected SynonymUpdater(final SynonymItem newItem) {
286             try {
287                 newFile = ComponentUtil.getSystemHelper().createTempFile(SYNONYM, ".txt");
288                 writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(newFile), Constants.UTF_8));
289             } catch (final Exception e) {
290                 if (newFile != null) {
291                     newFile.delete();
292                 }
293                 throw new DictionaryException("Failed to write a userDict file.", e);
294             }
295             item = newItem;
296         }
297 
298         public SynonymItem write(final SynonymItem oldItem) {
299             try {
300                 if ((item == null) || (item.getId() != oldItem.getId()) || !item.isUpdated()) {
301                     writer.write(oldItem.toLineString());
302                     writer.write(Constants.LINE_SEPARATOR);
303                     return oldItem;
304                 }
305                 if (!item.equals(oldItem)) {
306                     throw new DictionaryException("Synonym file was updated: old=" + oldItem + " : new=" + item);
307                 }
308                 try {
309                     if (!item.isDeleted()) {
310                         // update
311                         writer.write(item.toLineString());
312                         writer.write(Constants.LINE_SEPARATOR);
313                         return new SynonymItem(item.getId(), item.getNewInputs(), item.getNewOutputs());
314                     } else {
315                         return null;
316                     }
317                 } finally {
318                     item.setNewInputs(null);
319                     item.setNewOutputs(null);
320                 }
321             } catch (final IOException e) {
322                 throw new DictionaryException("Failed to write: " + oldItem + " -> " + item, e);
323             }
324         }
325 
326         public void write(final String line) {
327             try {
328                 writer.write(line);
329                 writer.write(Constants.LINE_SEPARATOR);
330             } catch (final IOException e) {
331                 throw new DictionaryException("Failed to write: " + line, e);
332             }
333         }
334 
335         public SynonymItem commit() {
336             isCommit = true;
337             if (item != null && item.isUpdated()) {
338                 try {
339                     writer.write(item.toLineString());
340                     writer.write(Constants.LINE_SEPARATOR);
341                     return item;
342                 } catch (final IOException e) {
343                     throw new DictionaryException("Failed to write: " + item, e);
344                 }
345             }
346             return null;
347         }
348 
349         @Override
350         public void close() {
351             try {
352                 writer.flush();
353             } catch (final IOException e) {
354                 // ignore
355             }
356             CloseableUtil.closeQuietly(writer);
357 
358             if (isCommit) {
359                 try {
360                     dictionaryManager.store(SynonymFile.this, newFile);
361                 } finally {
362                     newFile.delete();
363                 }
364             } else {
365                 newFile.delete();
366             }
367         }
368     }
369 
370 }