View Javadoc
1   /*
2    * Copyright 2012-2017 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.seunjeon;
17  
18  import java.io.BufferedInputStream;
19  import java.io.BufferedReader;
20  import java.io.BufferedWriter;
21  import java.io.Closeable;
22  import java.io.File;
23  import java.io.FileOutputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.InputStreamReader;
27  import java.io.OutputStreamWriter;
28  import java.io.Writer;
29  import java.util.ArrayList;
30  import java.util.Collections;
31  import java.util.Date;
32  import java.util.List;
33  
34  import org.apache.commons.io.IOUtils;
35  import org.codelibs.core.lang.StringUtil;
36  import org.codelibs.fess.Constants;
37  import org.codelibs.fess.dict.DictionaryException;
38  import org.codelibs.fess.dict.DictionaryFile;
39  import org.dbflute.optional.OptionalEntity;
40  
41  public class SeunjeonFile extends DictionaryFile<SeunjeonItem> {
42      private static final String SEUNJEON = "seunjeon";
43  
44      List<SeunjeonItem> seunjeonItemList;
45  
46      public SeunjeonFile(final String id, final String path, final Date timestamp) {
47          super(id, path, timestamp);
48      }
49  
50      @Override
51      public String getType() {
52          return SEUNJEON;
53      }
54  
55      @Override
56      public String getPath() {
57          return path;
58      }
59  
60      @Override
61      public synchronized OptionalEntity<SeunjeonItem> get(final long id) {
62          if (seunjeonItemList == null) {
63              reload(null, null);
64          }
65  
66          for (final SeunjeonItem SeunjeonItem : seunjeonItemList) {
67              if (id == SeunjeonItem.getId()) {
68                  return OptionalEntity.of(SeunjeonItem);
69              }
70          }
71          return OptionalEntity.empty();
72      }
73  
74      @Override
75      public synchronized PagingList<SeunjeonItem> selectList(final int offset, final int size) {
76          if (seunjeonItemList == null) {
77              reload(null, null);
78          }
79  
80          if (offset >= seunjeonItemList.size() || offset < 0) {
81              return new PagingList<>(Collections.<SeunjeonItem> emptyList(), offset, size, seunjeonItemList.size());
82          }
83  
84          int toIndex = offset + size;
85          if (toIndex > seunjeonItemList.size()) {
86              toIndex = seunjeonItemList.size();
87          }
88  
89          return new PagingList<>(seunjeonItemList.subList(offset, toIndex), offset, size, seunjeonItemList.size());
90      }
91  
92      @Override
93      public synchronized void insert(final SeunjeonItem item) {
94          try (SynonymUpdater updater = new SynonymUpdater(item)) {
95              reload(updater, null);
96          }
97      }
98  
99      @Override
100     public synchronized void update(final SeunjeonItem item) {
101         try (SynonymUpdater updater = new SynonymUpdater(item)) {
102             reload(updater, null);
103         }
104     }
105 
106     @Override
107     public synchronized void delete(final SeunjeonItem item) {
108         final SeunjeonItem SeunjeonItem = item;
109         SeunjeonItem.setNewInputs(StringUtil.EMPTY_STRINGS);
110         try (SynonymUpdater updater = new SynonymUpdater(item)) {
111             reload(updater, null);
112         }
113     }
114 
115     protected void reload(final SynonymUpdater updater, final InputStream in) {
116         final List<SeunjeonItem> itemList = new ArrayList<>();
117         try (BufferedReader reader =
118                 new BufferedReader(new InputStreamReader(in != null ? in : dictionaryManager.getContentInputStream(this), Constants.UTF_8))) {
119             long id = 0;
120             String line = null;
121             while ((line = reader.readLine()) != null) {
122                 if (line.length() == 0 || line.charAt(0) == '#') {
123                     if (updater != null) {
124                         updater.write(line);
125                     }
126                     continue; // ignore empty lines and comments
127                 }
128 
129                 final List<String> inputStrings = split(line, ",");
130                 final String[] inputs = new String[inputStrings.size()];
131                 for (int i = 0; i < inputs.length; i++) {
132                     inputs[i] = unescape(inputStrings.get(i)).trim();
133                 }
134 
135                 if (inputs.length > 0) {
136                     id++;
137                     final SeunjeonItem item = new SeunjeonItem(id, inputs);
138                     if (updater != null) {
139                         final SeunjeonItem newItem = updater.write(item);
140                         if (newItem != null) {
141                             itemList.add(newItem);
142                         } else {
143                             id--;
144                         }
145                     } else {
146                         itemList.add(item);
147                     }
148                 }
149             }
150             if (updater != null) {
151                 final SeunjeonItem item = updater.commit();
152                 if (item != null) {
153                     itemList.add(item);
154                 }
155             }
156             seunjeonItemList = itemList;
157         } catch (final IOException e) {
158             throw new DictionaryException("Failed to parse " + path, e);
159         }
160     }
161 
162     private static List<String> split(final String s, final String separator) {
163         final List<String> list = new ArrayList<>(2);
164         StringBuilder sb = new StringBuilder();
165         int pos = 0;
166         final int end = s.length();
167         while (pos < end) {
168             if (s.startsWith(separator, pos)) {
169                 if (sb.length() > 0) {
170                     list.add(sb.toString());
171                     sb = new StringBuilder();
172                 }
173                 pos += separator.length();
174                 continue;
175             }
176 
177             char ch = s.charAt(pos++);
178             if (ch == '\\') {
179                 sb.append(ch);
180                 if (pos >= end) {
181                     break; // ERROR, or let it go?
182                 }
183                 ch = s.charAt(pos++);
184             }
185 
186             sb.append(ch);
187         }
188 
189         if (sb.length() > 0) {
190             list.add(sb.toString());
191         }
192 
193         return list;
194     }
195 
196     private String unescape(final String s) {
197         if (s.indexOf('\\') >= 0) {
198             final StringBuilder sb = new StringBuilder();
199             for (int i = 0; i < s.length(); i++) {
200                 final char ch = s.charAt(i);
201                 if (ch == '\\' && i < s.length() - 1) {
202                     sb.append(s.charAt(++i));
203                 } else {
204                     sb.append(ch);
205                 }
206             }
207             return sb.toString();
208         }
209         return s;
210     }
211 
212     public String getSimpleName() {
213         return new File(path).getName();
214     }
215 
216     public InputStream getInputStream() throws IOException {
217         return new BufferedInputStream(dictionaryManager.getContentInputStream(this));
218     }
219 
220     public synchronized void update(final InputStream in) throws IOException {
221         try (SynonymUpdater updater = new SynonymUpdater(null)) {
222             reload(updater, in);
223         }
224     }
225 
226     @Override
227     public String toString() {
228         return "SynonymFile [path=" + path + ", seunjeonItemList=" + seunjeonItemList + ", id=" + id + "]";
229     }
230 
231     protected class SynonymUpdater implements Closeable {
232 
233         protected boolean isCommit = false;
234 
235         protected File newFile;
236 
237         protected Writer writer;
238 
239         protected SeunjeonItem item;
240 
241         protected SynonymUpdater(final SeunjeonItem newItem) {
242             try {
243                 newFile = File.createTempFile(SEUNJEON, ".txt");
244                 writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(newFile), Constants.UTF_8));
245             } catch (final IOException e) {
246                 if (newFile != null) {
247                     newFile.delete();
248                 }
249                 throw new DictionaryException("Failed to write a userDict file.", e);
250             }
251             item = newItem;
252         }
253 
254         public SeunjeonItem write(final SeunjeonItem oldItem) {
255             try {
256                 if (item != null && item.getId() == oldItem.getId() && item.isUpdated()) {
257                     if (item.equals(oldItem)) {
258                         try {
259                             if (!item.isDeleted()) {
260                                 // update
261                                 writer.write(item.toLineString());
262                                 writer.write(Constants.LINE_SEPARATOR);
263                                 return new SeunjeonItem(item.getId(), item.getNewInputs());
264                             } else {
265                                 return null;
266                             }
267                         } finally {
268                             item.setNewInputs(null);
269                         }
270                     } else {
271                         throw new DictionaryException("Seunjeon file was updated: old=" + oldItem + " : new=" + item);
272                     }
273                 } else {
274                     writer.write(oldItem.toLineString());
275                     writer.write(Constants.LINE_SEPARATOR);
276                     return oldItem;
277                 }
278             } catch (final IOException e) {
279                 throw new DictionaryException("Failed to write: " + oldItem + " -> " + item, e);
280             }
281         }
282 
283         public void write(final String line) {
284             try {
285                 writer.write(line);
286                 writer.write(Constants.LINE_SEPARATOR);
287             } catch (final IOException e) {
288                 throw new DictionaryException("Failed to write: " + line, e);
289             }
290         }
291 
292         public SeunjeonItem commit() {
293             isCommit = true;
294             if (item != null && item.isUpdated()) {
295                 try {
296                     writer.write(item.toLineString());
297                     writer.write(Constants.LINE_SEPARATOR);
298                     return item;
299                 } catch (final IOException e) {
300                     throw new DictionaryException("Failed to write: " + item, e);
301                 }
302             }
303             return null;
304         }
305 
306         @Override
307         public void close() {
308             try {
309                 writer.flush();
310             } catch (final IOException e) {
311                 // ignore
312             }
313             IOUtils.closeQuietly(writer);
314 
315             if (isCommit) {
316                 try {
317                     dictionaryManager.store(SeunjeonFile.this, newFile);
318                 } finally {
319                     newFile.delete();
320                 }
321             } else {
322                 newFile.delete();
323             }
324         }
325     }
326 }