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.mapping;
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  import java.util.regex.Matcher;
33  import java.util.regex.Pattern;
34  
35  import org.apache.logging.log4j.LogManager;
36  import org.apache.logging.log4j.Logger;
37  import org.codelibs.core.io.CloseableUtil;
38  import org.codelibs.core.lang.StringUtil;
39  import org.codelibs.curl.CurlResponse;
40  import org.codelibs.fess.Constants;
41  import org.codelibs.fess.dict.DictionaryException;
42  import org.codelibs.fess.dict.DictionaryFile;
43  import org.codelibs.fess.util.ComponentUtil;
44  import org.dbflute.optional.OptionalEntity;
45  
46  public class CharMappingFile extends DictionaryFile<CharMappingItem> {
47      private static final Logger logger = LogManager.getLogger(CharMappingFile.class);
48  
49      private static final String MAPPING = "mapping";
50  
51      List<CharMappingItem> mappingItemList;
52  
53      public CharMappingFile(final String id, final String path, final Date timestamp) {
54          super(id, path, timestamp);
55      }
56  
57      @Override
58      public String getType() {
59          return MAPPING;
60      }
61  
62      @Override
63      public String getPath() {
64          return path;
65      }
66  
67      @Override
68      public OptionalEntity<CharMappingItem> get(final long id) {
69          if (mappingItemList == null) {
70              reload(null);
71          }
72  
73          for (final CharMappingItem mappingItem : mappingItemList) {
74              if (id == mappingItem.getId()) {
75                  return OptionalEntity.of(mappingItem);
76              }
77          }
78          return OptionalEntity.empty();
79      }
80  
81      @Override
82      public synchronized PagingList<CharMappingItem> selectList(final int offset, final int size) {
83          if (mappingItemList == null) {
84              reload(null);
85          }
86  
87          if (offset >= mappingItemList.size() || offset < 0) {
88              return new PagingList<>(Collections.<CharMappingItem> emptyList(), offset, size, mappingItemList.size());
89          }
90  
91          int toIndex = offset + size;
92          if (toIndex > mappingItemList.size()) {
93              toIndex = mappingItemList.size();
94          }
95  
96          return new PagingList<>(mappingItemList.subList(offset, toIndex), offset, size, mappingItemList.size());
97      }
98  
99      @Override
100     public synchronized void insert(final CharMappingItem item) {
101         try (MappingUpdater updater = new MappingUpdater(item)) {
102             reload(updater);
103         }
104     }
105 
106     @Override
107     public synchronized void update(final CharMappingItem item) {
108         try (MappingUpdater updater = new MappingUpdater(item)) {
109             reload(updater);
110         }
111     }
112 
113     @Override
114     public synchronized void delete(final CharMappingItem item) {
115         final CharMappingItem mappingItem = item;
116         mappingItem.setNewInputs(StringUtil.EMPTY_STRINGS);
117         mappingItem.setNewOutput(StringUtil.EMPTY);
118         try (MappingUpdater updater = new MappingUpdater(item)) {
119             reload(updater);
120         }
121     }
122 
123     protected void reload(final MappingUpdater updater) {
124         try (CurlResponse curlResponse = dictionaryManager.getContentResponse(this)) {
125             reload(updater, curlResponse.getContentAsStream());
126         } catch (final IOException e) {
127             throw new DictionaryException("Failed to parse " + path, e);
128         }
129     }
130 
131     protected void reload(final MappingUpdater updater, final InputStream in) {
132         final Pattern parsePattern = Pattern.compile("(.*)\\s*=>\\s*(.*)\\s*$");
133         final List<CharMappingItem> itemList = new ArrayList<>();
134         try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, Constants.UTF_8))) {
135             long id = 0;
136             String line = null;
137             while ((line = reader.readLine()) != null) {
138                 // Remove comments
139                 final String replacedLine = line.replaceAll("#.*$", StringUtil.EMPTY).trim();
140 
141                 // Skip empty lines or comment lines
142                 if (replacedLine.length() == 0) {
143                     if (updater != null) {
144                         updater.write(line);
145                     }
146                     continue;
147                 }
148 
149                 String[] inputs;
150                 String output;
151 
152                 final Matcher m = parsePattern.matcher(replacedLine);
153 
154                 if (!m.find()) {
155                     logger.warn("Failed to parse {} in {}", line, path);
156                     if (updater != null) {
157                         updater.write("# " + line);
158                     }
159                     continue;
160                 }
161 
162                 inputs = m.group(1).trim().split(",");
163                 output = m.group(2).trim();
164 
165                 if (inputs == null || output == null || inputs.length == 0) {
166                     logger.warn("Failed to parse {} in {}", line, path);
167                     if (updater != null) {
168                         updater.write("# " + line);
169                     }
170                     continue;
171                 }
172 
173                 id++;
174                 final CharMappingItem item = new CharMappingItem(id, inputs, output);
175 
176                 if (updater != null) {
177                     final CharMappingItem newItem = updater.write(item);
178                     if (newItem != null) {
179                         itemList.add(newItem);
180                     } else {
181                         id--;
182                     }
183                 } else {
184                     itemList.add(item);
185                 }
186             }
187             if (updater != null) {
188                 final CharMappingItem item = updater.commit();
189                 if (item != null) {
190                     itemList.add(item);
191                 }
192             }
193             mappingItemList = itemList;
194         } catch (final IOException e) {
195             throw new DictionaryException("Failed to parse " + path, e);
196         }
197     }
198 
199     public String getSimpleName() {
200         return new File(path).getName();
201     }
202 
203     public synchronized void update(final InputStream in) throws IOException {
204         try (MappingUpdater updater = new MappingUpdater(null)) {
205             reload(updater, in);
206         }
207     }
208 
209     @Override
210     public String toString() {
211         return "MappingFile [path=" + path + ", mappingItemList=" + mappingItemList + ", id=" + id + "]";
212     }
213 
214     protected class MappingUpdater implements Closeable {
215 
216         protected boolean isCommit = false;
217 
218         protected File newFile;
219 
220         protected Writer writer;
221 
222         protected CharMappingItem item;
223 
224         protected MappingUpdater(final CharMappingItem newItem) {
225             try {
226                 newFile = ComponentUtil.getSystemHelper().createTempFile(MAPPING, ".txt");
227                 writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(newFile), Constants.UTF_8));
228             } catch (final Exception e) {
229                 if (newFile != null) {
230                     newFile.delete();
231                 }
232                 throw new DictionaryException("Failed to write a userDict file.", e);
233             }
234             item = newItem;
235         }
236 
237         public CharMappingItem write(final CharMappingItem oldItem) {
238             try {
239                 if ((item == null) || (item.getId() != oldItem.getId()) || !item.isUpdated()) {
240                     writer.write(oldItem.toLineString());
241                     writer.write(Constants.LINE_SEPARATOR);
242                     return oldItem;
243                 }
244                 if (!item.equals(oldItem)) {
245                     throw new DictionaryException("Mapping file was updated: old=" + oldItem + " : new=" + item);
246                 }
247                 try {
248                     if (!item.isDeleted()) {
249                         // update
250                         writer.write(item.toLineString());
251                         writer.write(Constants.LINE_SEPARATOR);
252                         return new CharMappingItem(item.getId(), item.getNewInputs(), item.getNewOutput());
253                     } else {
254                         return null;
255                     }
256                 } finally {
257                     item.setNewInputs(null);
258                     item.setNewOutput(null);
259                 }
260             } catch (final IOException e) {
261                 throw new DictionaryException("Failed to write: " + oldItem + " -> " + item, e);
262             }
263         }
264 
265         public void write(final String line) {
266             try {
267                 writer.write(line);
268                 writer.write(Constants.LINE_SEPARATOR);
269             } catch (final IOException e) {
270                 throw new DictionaryException("Failed to write: " + line, e);
271             }
272         }
273 
274         public CharMappingItem commit() {
275             isCommit = true;
276             if (item != null && item.isUpdated()) {
277                 try {
278                     writer.write(item.toLineString());
279                     writer.write(Constants.LINE_SEPARATOR);
280                     return item;
281                 } catch (final IOException e) {
282                     throw new DictionaryException("Failed to write: " + item, e);
283                 }
284             }
285             return null;
286         }
287 
288         @Override
289         public void close() {
290             try {
291                 writer.flush();
292             } catch (final IOException e) {
293                 // ignore
294             }
295             CloseableUtil.closeQuietly(writer);
296 
297             if (isCommit) {
298                 try {
299                     dictionaryManager.store(CharMappingFile.this, newFile);
300                 } finally {
301                     newFile.delete();
302                 }
303             } else {
304                 newFile.delete();
305             }
306         }
307     }
308 
309 }