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