1 package org.coolreader.genrescollection;
2 
3 import android.content.Context;
4 import android.content.res.Resources;
5 import android.content.res.XmlResourceParser;
6 import android.util.Log;
7 
8 import org.xmlpull.v1.XmlPullParser;
9 import org.xmlpull.v1.XmlPullParserException;
10 
11 import java.io.IOException;
12 import java.util.ArrayList;
13 import java.util.HashMap;
14 import java.util.LinkedHashMap;
15 import java.util.List;
16 import java.util.Map;
17 import java.util.Stack;
18 
19 public class GenresCollection {
20 
21 	private static final String TAG = "genre";
22 
23 	static public final class GenreRecord {
24 		private final int m_id;
25 		private final String m_code;
26 		private String m_name;
27 		private List<GenreRecord> m_childs;
28 		private int m_level;
29 		private List<String> m_aliases;
30 
GenreRecord(int id, String code, String name, int level)31 		private GenreRecord(int id, String code, String name, int level) {
32 			m_id = id;
33 			m_code = code;
34 			m_name = name;
35 			m_level = level;
36 			m_childs = new ArrayList<>();
37 			m_aliases = new ArrayList<>();
38 		}
39 
getId()40 		public int getId() {
41 			return m_id;
42 		}
43 
getCode()44 		public String getCode() {
45 			return m_code;
46 		}
47 
getName()48 		public String getName() {
49 			return m_name;
50 		}
51 
setName(String name)52 		public void setName(String name) {
53 			m_name = name;
54 		}
55 
getLevel()56 		public int getLevel() {
57 			return m_level;
58 		}
59 
getChilds()60 		public List<GenreRecord> getChilds() {
61 			return m_childs;
62 		}
63 
hasChilds()64 		public boolean hasChilds() {
65 			return null != m_childs && !m_childs.isEmpty();
66 		}
67 
contain(String code)68 		public boolean contain(String code) {
69 			if (m_code.equals(code))
70 				return true;
71 			for (GenreRecord record : m_childs) {
72 				if (record.getCode().equals(code))
73 					return true;
74 			}
75 			return false;
76 		}
77 
hasAliases()78 		public boolean hasAliases() {
79 			return null != m_aliases && !m_aliases.isEmpty();
80 		}
81 
getAliases()82 		public List<String> getAliases() {
83 			return m_aliases;
84 		}
85 
addAlias(String alias)86 		private boolean addAlias(String alias) {
87 			boolean exist = false;
88 			for (String a : m_aliases) {
89 				if (a.equals(alias)) {
90 					exist = true;
91 					break;
92 				}
93 			}
94 			if (!exist) {
95 				m_aliases.add(alias);
96 			}
97 			return !exist;
98 		}
99 	}
100 
101 	// main container
102 	private Map<String, GenreRecord> m_collection;
103 	// key: genre code
104 	// value: genre record
105 	private Map<String, GenreRecord> m_allGenres;
106 	// instance
107 	private static GenresCollection m_instance = null;
108 	private int m_version;
109 
GenresCollection()110 	private GenresCollection() {
111 		m_collection = new LinkedHashMap<>();
112 		m_allGenres = new HashMap<>();
113 		m_version = 0;
114 	}
115 
addGenre(int groupId, String groupCode, String groupName, int id, String code, String name)116 	private void addGenre(int groupId, String groupCode, String groupName, int id, String code, String name) {
117 		GenreRecord group = m_collection.get(groupCode);
118 		if (null == group) {
119 			group = new GenreRecord(groupId, groupCode, groupName, 0);
120 			m_collection.put(groupCode, group);
121 			m_allGenres.put(groupCode, group);
122 		}
123 		List<GenreRecord> groupChilds = group.getChilds();
124 		// Check if already exist in this group
125 		GenreRecord exist = null;
126 		for (GenreRecord rec : groupChilds) {
127 			if (rec.getCode().equals(code)) {
128 				exist = rec;
129 				break;
130 			}
131 		}
132 		if (null == exist) {
133 			// add new child
134 			GenreRecord record = m_allGenres.get(code);
135 			if (null != record) {
136 				if (id != record.getId()) {
137 					Log.w(TAG, "genres: trying to add already existing genre '" + code + "' with different id: " + id + " != " + record.getId() + "!");
138 					Log.w(TAG, "genres: new id will be ignored.");
139 				//} else {
140 				//	Log.d(TAG, "genres: for genre '" + code + "' duplicate found.");
141 				}
142 			} else {
143 				record = new GenreRecord(id, code, name, 1);
144 				m_allGenres.put(code, record);
145 			}
146 			groupChilds.add(record);
147 		} else {
148 			Log.w(TAG, "genres: genre with code '" + code + "' already exist in genre group '" + groupCode + "', skipped.");
149 		}
150 	}
151 
addAliases(String genreCode, List<String> aliases)152 	private void addAliases(String genreCode, List<String> aliases) {
153 		GenreRecord genre = byCode(genreCode);
154 		if (null != genre) {
155 			for (String alias : aliases) {
156 				if (genre.addAlias(alias))
157 					m_allGenres.put(alias, genre);
158 			}
159 		} else {
160 			Log.w(TAG, "No such genre '" + genreCode + "' to register aliases!");
161 		}
162 	}
163 
getVersion()164 	public int getVersion() {
165 		return m_version;
166 	}
167 
byCode(String code)168 	public GenreRecord byCode(String code) {
169 		return m_allGenres.get(code);
170 	}
171 
byId(String strId)172 	public GenreRecord byId(String strId) {
173 		int id = -1;
174 		try {
175 			id = Integer.parseInt(strId, 10);
176 		} catch (Exception ignored) {
177 		}
178 		if (id > 0)
179 			return byId(id);
180 		return null;
181 	}
182 
byId(int id)183 	public GenreRecord byId(int id) {
184 		GenreRecord genre = null;
185 		for (Map.Entry<String, GenreRecord> entry : m_allGenres.entrySet()) {
186 			if (entry.getValue().getId() == id) {
187 				genre = entry.getValue();
188 				break;
189 			}
190 		}
191 		return genre;
192 	}
193 
translate(String code)194 	public String translate(String code) {
195 		GenreRecord record = m_allGenres.get(code);
196 		if (null != record)
197 			return record.getName();
198 		// If not found, return as is.
199 		return code;
200 	}
201 
getCollection()202 	public Map<String, GenreRecord> getCollection() {
203 		return m_collection;
204 	}
205 
getGroups()206 	public List<String> getGroups() {
207 		ArrayList<String> list = new ArrayList<>();
208 		for (Map.Entry<String, GenreRecord> entry : m_collection.entrySet()) {
209 			list.add(entry.getKey());
210 		}
211 		return list;
212 	}
213 
getInstance(Context context)214 	public static GenresCollection getInstance(Context context) {
215 		if (null == m_instance) {
216 			m_instance = new GenresCollection();
217 			m_instance.loadGenresFromResource(context);
218 		}
219 		return m_instance;
220 	}
221 
reloadGenresFromResource(Context context)222 	public static boolean reloadGenresFromResource(Context context) {
223 		if (null == m_instance) {
224 			m_instance = new GenresCollection();
225 			return m_instance.loadGenresFromResource(context);
226 		}
227 		return m_instance.loadGenresFromResource(context);
228 	}
229 
loadGenresFromResource(Context context)230 	private boolean loadGenresFromResource(Context context) {
231 		boolean res = false;
232 		try {
233 			XmlResourceParser parser = context.getResources().getXml(R.xml.union_genres);
234 			// parser data
235 			Stack<String> tagStack = new Stack<>();
236 			String tag;
237 			String text = null;
238 			String parentTag;
239 			int groupId = -1;
240 			String groupCode = null;
241 			String groupName = null;
242 			String str;
243 			int genreId = -1;
244 			String genreCode = null;
245 			String genreName = null;
246 			int count = 0;
247 			// genre aliases
248 			String aliasForCode = null;
249 			String aliasCode;
250 			// key: genre code
251 			// value: list of aliases
252 			HashMap<String, ArrayList<String>> allAliases = new HashMap<>();
253 
254 			// start to parse
255 			parser.next();
256 			int eventType = parser.getEventType();
257 			while (eventType != XmlPullParser.END_DOCUMENT) {
258 				switch (eventType) {
259 					case XmlPullParser.START_DOCUMENT:
260 						//Log.d(TAG, "START_DOCUMENT");
261 						break;
262 					case XmlPullParser.START_TAG:
263 						tag = parser.getName();
264 						//Log.d(TAG, "START_TAG: " + tag);
265 						if (!tagStack.empty())
266 							parentTag = tagStack.peek();
267 						else
268 							parentTag = "";
269 						tagStack.push(tag);
270 						if ("genres".equals(tag)) {
271 							if (parentTag.length() == 0) {
272 								// root tag found, clearing genre's collection
273 								m_collection = new LinkedHashMap<>();
274 								m_version = -1;
275 								str = parser.getAttributeValue(null, "version");
276 								if (null != str) {
277 									try {
278 										m_version = Integer.parseInt(str, 10);
279 									} catch (Exception e) {
280 										throw new XmlPullParserException(e.toString());
281 									}
282 								}
283 								if (m_version < 1)
284 									throw new XmlPullParserException("Invalid resource version: " + str);
285 							} else {
286 								throw new XmlPullParserException("the element 'genres' must be the root element!");
287 							}
288 						} else if ("group".equals(tag)) {
289 							if ("genres".equals(parentTag)) {
290 								groupCode = parser.getAttributeValue(null, "code");
291 								groupName = parser.getAttributeValue(null, "name");
292 								if (null != groupName) {
293 									groupName = resolveStringResource(context, groupName);
294 								}
295 								groupId = -1;
296 								str = parser.getAttributeValue(null, "id");
297 								if (null != str) {
298 									try {
299 										groupId = Integer.parseInt(str, 10);
300 									} catch (Exception e) {
301 										throw new XmlPullParserException(e.toString());
302 									}
303 								}
304 								if (groupId < 0)
305 									throw new XmlPullParserException("Invalid group id: " + str);
306 							} else {
307 								throw new XmlPullParserException("the 'group' element must only be inside the 'genres' element!");
308 							}
309 						} else if ("genre".equals(tag)) {
310 							if ("group".equals(parentTag)) {
311 								genreCode = parser.getAttributeValue(null, "code");
312 								genreName = parser.getAttributeValue(null, "name");
313 								if (null != genreName && genreName.length() > 0) {
314 									genreName = resolveStringResource(context, genreName);
315 								}
316 								genreId = -1;
317 								str = parser.getAttributeValue(null, "id");
318 								if (null != str) {
319 									try {
320 										genreId = Integer.parseInt(str, 10);
321 									} catch (Exception e) {
322 										throw new XmlPullParserException(e.toString());
323 									}
324 								}
325 								if (genreId < 0)
326 									throw new XmlPullParserException("Invalid genre id: " + str);
327 							} else {
328 								throw new XmlPullParserException("the 'genre' element must only be inside the 'group' element!");
329 							}
330 						} else if ("aliases".equals(tag)) {
331 							if ("genres".equals(parentTag)) {
332 								aliasForCode = null;
333 							} else {
334 								throw new XmlPullParserException("the 'aliases' element must only be inside the 'genres' element!");
335 							}
336 						} else if ("alias".equals(tag)) {
337 							if ("aliases".equals(parentTag)) {
338 								aliasForCode = parser.getAttributeValue(null, "code");
339 							} else {
340 								throw new XmlPullParserException("the 'alias' element must only be inside the 'aliases' element!");
341 							}
342 						}
343 						text = "";
344 						break;
345 					case XmlPullParser.END_TAG:
346 						//Log.d(TAG, "END_TAG: " + parser.getName());
347 						if (!tagStack.empty())
348 							tag = tagStack.pop();
349 						else
350 							tag = "";
351 						if (!tag.equals(parser.getName())) {
352 							throw new XmlPullParserException("end element '" + parser.getName() + "' not equal to start element '" + tag + "'");
353 						}
354 						switch (tag) {
355 							case "genre":
356 								if (null != groupCode && groupCode.length() > 0 &&
357 										null != groupName && groupName.length() > 0 &&
358 										null != genreCode && genreCode.length() > 0 &&
359 										null != genreName && genreName.length() > 0) {
360 									addGenre(groupId, groupCode, groupName, genreId, genreCode, genreName);
361 									count++;
362 								}
363 								genreId = -1;
364 								genreCode = null;
365 								genreName = null;
366 								break;
367 							case "group":
368 								groupId = -1;
369 								groupCode = null;
370 								groupName = null;
371 								break;
372 							case "alias":
373 								// save alias to temporary container
374 								aliasCode = text;
375 								if (null != aliasForCode && aliasForCode.length() > 0 &&
376 										null != aliasCode && aliasCode.length() > 0) {
377 									ArrayList<String> aliases = allAliases.get(aliasForCode);
378 									if (null == aliases) {
379 										aliases = new ArrayList<>();
380 										allAliases.put(aliasForCode, aliases);
381 									}
382 									// don't check already exist aliases here
383 									// checked in GenreRecord.addAlias()
384 									aliases.add(aliasCode);
385 								}
386 								aliasForCode = null;
387 								break;
388 						}
389 						break;
390 					case XmlPullParser.TEXT:
391 						//Log.d(TAG, "TEXT: " + parser.getText());
392 						text = parser.getText();
393 						break;
394 				}
395 				eventType = parser.next();
396 			}
397 			// Only after all genres are registered, add aliases
398 			for (Map.Entry<String, ArrayList<String>> entry : allAliases.entrySet()) {
399 				addAliases(entry.getKey(), entry.getValue());
400 			}
401 			res = count > 0;
402 		} catch (IndexOutOfBoundsException e) {
403 			e.printStackTrace();
404 		} catch (IOException e) {
405 			e.printStackTrace();
406 		} catch (XmlPullParserException e) {
407 			e.printStackTrace();
408 		} catch (Resources.NotFoundException e) {
409 			e.printStackTrace();
410 		} catch (NullPointerException e) {
411 			e.printStackTrace();
412 		}
413 		return res;
414 	}
415 
resolveStringResource(Context context, String resCode)416 	private static String resolveStringResource(Context context, String resCode) {
417 		if (null != resCode && resCode.startsWith("@")) {
418 			try {
419 				int resId = Integer.parseInt(resCode.substring(1), 10);
420 				if (resId != 0) {
421 					String str = context.getString(resId);
422 					if (null != str && str.length() > 0)
423 						return str;
424 				}
425 			} catch (NumberFormatException ignored) {
426 				// ignore this exception, return original string
427 			}
428 		}
429 		return resCode;
430 	}
431 }
432