View Javadoc

1   package cz.cuni.amis.utils;
2   
3   import java.io.BufferedReader;
4   import java.io.File;
5   import java.io.FileInputStream;
6   import java.io.FileWriter;
7   import java.io.IOException;
8   import java.io.InputStream;
9   import java.io.InputStreamReader;
10  import java.io.PrintWriter;
11  import java.io.StringWriter;
12  import java.text.SimpleDateFormat;
13  import java.util.ArrayList;
14  import java.util.Collection;
15  import java.util.Collections;
16  import java.util.Comparator;
17  import java.util.Date;
18  import java.util.HashMap;
19  import java.util.Iterator;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.Set;
23  import java.util.TreeMap;
24  import java.util.logging.Logger;
25  
26  import cz.cuni.amis.utils.exception.PogamutException;
27  import cz.cuni.amis.utils.exception.PogamutIOException;
28  
29  public class IniFile {
30  	
31  	public static abstract class SectionEntry {
32  		
33  		private int sectionEntryIndex;
34  		
35  		public SectionEntry(int sectionEntryIndex) {
36  			this.sectionEntryIndex = sectionEntryIndex;
37  		}
38  				
39  		public int getSectionEntryIndex() {
40  			return sectionEntryIndex;
41  		}
42  		
43  		protected void setSectionEntryIndex(int sectionEntryIndex) {
44  			this.sectionEntryIndex = sectionEntryIndex;
45  		}
46  		
47  		public abstract String getIniFileLines();
48  		
49  		public abstract String getKey();
50  		
51  	}
52  	
53  	public static class SectionEntryComment extends SectionEntry {
54  		
55  		private String comment;
56  		private String text;
57  		private String key = null;
58  
59  		public SectionEntryComment(int sectionEntryIndex, String text, String comment) {
60  			super(sectionEntryIndex);
61  			NullCheck.check(text, "text");
62  			if (!text.startsWith(";")) text = ";" + text;
63  			this.text = text;
64  			// HAS KEY?
65  			int separ = text.indexOf("=");
66  			if (separ < 0) return;
67  			key = text.substring(0, separ);
68  			if (key == null || key.isEmpty()) return;
69  			while (key.startsWith(";")) key = key.substring(1);			
70  		}
71  
72  		public String getComment() {
73  			return comment;
74  		}
75  
76  		public void setComment(String comment) {
77  			this.comment = comment;
78  		}
79  
80  		public String getText() {
81  			return text;
82  		}
83  
84  		public void setText(String text) {
85  			this.text = text;
86  		}
87  
88  		@Override
89  		public String getIniFileLines() {
90  			if (comment != null) {
91  				return comment + "\n" + text;
92  			}
93  			return text;
94  		}
95  
96  		@Override
97  		public String getKey() {
98  			return key;
99  		}
100 			
101 	}
102 	
103 	public static class SectionEntryKeyValue extends SectionEntry {
104 		
105 		private String comment;
106 		private String key;
107 		private List<String> values = new ArrayList<String>();
108 
109 		public SectionEntryKeyValue(int sectionEntryIndex, String key, String value, String comment) {
110 			super(sectionEntryIndex);
111 			this.key = key;
112 			this.values.add(value);
113 			this.comment = comment;
114 			if (this.comment != null) {
115 				if (!this.comment.startsWith(";")) this.comment = ";" + this.comment;
116 			}
117 			NullCheck.check(this.key, "key");
118 			NullCheck.check(value, "value");
119 		}
120 		
121 		public SectionEntryKeyValue(int sectionEntryIndex, String key, List<String> values, String comment) {
122 			super(sectionEntryIndex);
123 			this.key = key;
124 			NullCheck.check(values, "values");
125 			this.values.addAll(values);
126 			this.comment = comment;
127 			if (this.comment != null) {
128 				if (!this.comment.startsWith(";")) this.comment = ";" + this.comment;
129 			}
130 			NullCheck.check(this.key, "key");
131 		}
132 		
133 		@Override
134 		public String getKey() {
135 			return key;
136 		}
137 
138 		public String getValue() {
139 			return values.size() > 0 ? values.get(0) : null;
140 		}
141 		
142 		public List<String> getValues() {
143 			return values;
144 		}
145 
146 		public void addValue(String value) {
147 			values.add(value);
148 		}
149 		
150 		public void setValue(String value) {
151 			values.clear();
152 			values.add(value);
153 		}
154 		
155 		public String getComment() {
156 			return comment;
157 		}
158 
159 		public void setComment(String comment) {
160 			this.comment = comment;
161 		}
162 
163 		@Override
164 		public String getIniFileLines() {
165 			StringBuffer sb = new StringBuffer();
166 			if (comment != null) {
167 				sb.append(comment);
168 				sb.append("\n");
169 			}
170 			boolean first = false;
171 			for (String value : values) {				
172 				if (first) first = false;
173 				else sb.append("\n");
174 				sb.append(key);
175 				sb.append("=");
176 				sb.append(value);
177 			}
178 			return sb.toString();
179 		}
180 		
181 	}
182 
183 	public static final Comparator<SectionEntry> SECTION_ENTRY_INDEX_COMPARATOR = new Comparator<SectionEntry>() {
184 		
185 		@Override
186 		public int compare(SectionEntry o1, SectionEntry o2) {
187 			return o1.getSectionEntryIndex() - o2.getSectionEntryIndex();
188 		}
189 	};
190 	
191 	public static final Comparator<SectionEntry> SECTION_ENTRY_KEY_COMPARATOR = new Comparator<SectionEntry>() {
192 		
193 		@Override
194 		public int compare(SectionEntry o1, SectionEntry o2) {
195 			int val = o1.getKey().compareTo(o2.getKey());
196 			if (val != 0) return val;			
197 			if (o1 instanceof SectionEntryKeyValue) {
198 				if (o2 instanceof SectionEntryComment) return -1;
199 				return 0;
200 			} else
201 			if (o1 instanceof SectionEntryComment) {
202 				if (o2 instanceof SectionEntryKeyValue) return 1;
203 				return 0;
204 			} else {
205 				return 0;
206 			}		
207 		}
208 	};
209 		
210 	/**
211 	 * Class representing one section of the ini file.
212 	 * @author Jimmy
213 	 */
214 	public static class Section {
215 		
216 		private static int nextSectionIndex = 0;
217 		private int nextSectionEntryIndex = 0;
218 		private int sectionIndex;
219 		private String name;
220 		
221 		private List<SectionEntryComment> comments = new ArrayList<SectionEntryComment>();
222 		
223 		private Map<String, SectionEntryKeyValue> props = new HashMap<String, SectionEntryKeyValue>();
224 		
225 		/**
226 		 * Creates a section of the given name.
227 		 * <p><p>
228 		 * Name can't be null!
229 		 * 
230 		 * @param name
231 		 */
232 		public Section(String name) {
233 			this.name = name;			
234 			NullCheck.check(this.name, "name");
235 			this.sectionIndex = nextSectionIndex++;
236 		}
237 
238 		/**
239 		 * Copy-constructor.
240 		 * @param section
241 		 */
242 		public Section(Section section) {
243 			this.name = section.getName();
244 			this.sectionIndex = nextSectionIndex++;
245 			this.add(section);
246 		}
247 
248 		/**
249 		 * Returns name of the section.
250 		 * @return
251 		 */
252 		public String getName() {
253 			return name;
254 		}
255 		
256 		/**
257 		 * Sets a property key=value into the section.
258 		 * @param key
259 		 * @param value
260 		 * @return this
261 		 */
262 		public Section put(String key, String value) {
263 			return put(key, value, null);
264 		}
265 		
266 		/**
267 		 * Sets a property key=value into the section with comment.
268 		 * @param key
269 		 * @param value
270 		 * @param comment
271 		 * @return this
272 		 */
273 		public Section put(String key, String value, String comment) {
274 			NullCheck.check(key, "key");
275 			SectionEntryKeyValue entry = props.get(key);
276 			if (entry != null) {
277 				entry.setValue(value);
278 			} else {
279 				entry = new SectionEntryKeyValue(nextSectionEntryIndex++, key, value, comment);
280 				props.put(key, entry);
281 			}
282 			return this;
283 		}
284 		
285 		/**
286 		 * Adds a property key=value into the section.
287 		 * @param key
288 		 * @param value
289 		 * @return this
290 		 */
291 		public Section add(String key, String value) {
292 			return add(key, value, null);
293 		}
294 		
295 		/**
296 		 * Adds a property key=value into the section with comment.
297 		 * @param key
298 		 * @param value
299 		 * @param comment
300 		 * @return this
301 		 */
302 		public Section add(String key, String value, String comment) {
303 			NullCheck.check(key, "key");
304 			SectionEntryKeyValue entry = props.get(key);
305 			if (entry != null) {
306 				entry.addValue(value);
307 			} else {
308 				entry = new SectionEntryKeyValue(nextSectionEntryIndex++, key, value, comment);
309 				props.put(key, entry);
310 			}
311 			return this;
312 		}
313 		
314 		/**
315 		 * Returns a value of the propety with 'key'.
316 		 * @param key
317 		 * @return
318 		 */
319 		public String getOne(String key) {
320 			SectionEntryKeyValue entry = props.get(key);
321 			if (entry == null) return null;
322 			return entry.getValue();
323 		}
324 		
325 		/**
326 		 * Returns a value of the propety with 'key'.
327 		 * @param key
328 		 * @return
329 		 */
330 		public List<String> getAll(String key) {
331 			SectionEntryKeyValue entry = props.get(key);
332 			if (entry == null) return null;
333 			return entry.getValues();
334 		}
335 		
336 		/**
337 		 * Returns full section entry for a 'key'.
338 		 * @param key
339 		 * @return
340 		 */
341 		public SectionEntryKeyValue getEntry(String key) {
342 			SectionEntryKeyValue entry = props.get(key);
343 			if (entry == null) return null;
344 			return entry;
345 		}
346 		
347 		/**
348 		 * Whether the section contains property of the given key.
349 		 * @param key
350 		 * @return
351 		 */
352 		public boolean containsKey(String key) {
353 			return props.containsKey(key);
354 		}
355 		
356 		/**
357 		 * Returns all keys stored within the map.
358 		 * @return
359 		 */
360 		public Set<String> getKeys() {
361 			return props.keySet();
362 		}
363 		
364 		/**
365 		 * Alias for {@link Section#getKeys()}.
366 		 * @return
367 		 */
368 		public Set<String> keySet() {
369 			return getKeys();
370 		}
371 		
372 		/**
373 		 * Removes a property under the 'key' from this section.
374 		 * 
375 		 * Time complexity O(n) due to section-entry-index consolidation.
376 		 * 
377 		 * @param key
378 		 * @return removed {@link SectionEntryKeyValue}
379 		 */
380 		public SectionEntryKeyValue remove(String key) {
381 			SectionEntryKeyValue entry = props.remove(key);
382 			indexDeleted(entry.getSectionEntryIndex());
383 			return entry;
384 		}
385 		
386 		private void indexDeleted(int sectionEntryIndex) {
387 			for (SectionEntryComment comment : comments) {
388 				if (comment.getSectionEntryIndex() > sectionEntryIndex) {
389 					comment.setSectionEntryIndex(comment.getSectionEntryIndex() - 1);
390 				}
391 			}
392 			for (SectionEntryKeyValue keyValue : props.values()) {
393 				if (keyValue.getSectionEntryIndex() > sectionEntryIndex) {
394 					keyValue.setSectionEntryIndex(keyValue.getSectionEntryIndex() - 1);
395 				}
396 			}
397 		}
398 
399 		/**
400 		 * Deletes all properties within this section.
401 		 * @return
402 		 */
403 		public Section clear() {
404 			props.clear();
405 			comments.clear();
406 			return this;
407 		}
408 		
409 		/**
410 		 * Deletes all comments within this section.
411 		 * @return
412 		 */
413 		public Section clearComments() {
414 			comments.clear();
415 			return this;
416 		}
417 
418 		/**
419 		 * Alias for {@link Section#put(String, String)}.
420 		 * @param key
421 		 * @param value
422 		 * @return this
423 		 */
424 		public Section set(String key, String value) {		
425 			return put(key, value);
426 		}
427 		
428 		/**
429 		 * Alias for {@link Section#put(String, Strin, String)}.
430 		 * @param key
431 		 * @param value
432 		 * @param comment
433 		 * @return this
434 		 */
435 		public Section set(String key, String value, String comment) {		
436 			return put(key, value, comment);
437 		}
438 		
439 		/**
440 		 * Adds comment to this section.
441 		 * @param comment
442 		 */
443 		public void addComment(String comment) {
444 			if (!comment.startsWith(";")) comment = ";" + comment;
445 			if (!isComment(comment)) throw new RuntimeException("'" + comment + "' is not a comment!");
446 			comments.add(new SectionEntryComment(nextSectionEntryIndex++, comment, null));
447 		}
448 		
449 		public void addComment(String keyValueCommented, String comment) {			
450 			if (!keyValueCommented.startsWith(";")) keyValueCommented = ";" + keyValueCommented;
451 			if (comment == null) {
452 				addComment(keyValueCommented);
453 				return;
454 			}
455 			if (!comment.startsWith(";")) comment = ";" + comment;
456 			if (!isComment(keyValueCommented)) throw new RuntimeException("'" + keyValueCommented + "' is not a comment!");
457 			if (!hasCommentKey(keyValueCommented)) throw new RuntimeException("'" + keyValueCommented + "' is not commented out key=value!");
458 			comments.add(new SectionEntryComment(nextSectionEntryIndex++, keyValueCommented, comment));
459 		}
460 
461 		/**
462 		 * Adds all properties from 'section' into this one.
463 		 * @param section
464 		 * @return 
465 		 * @return this
466 		 */
467 		public Section add(Section section) {
468 			for (SectionEntryKeyValue keyValue : props.values()) {
469 				put(keyValue.getKey(), keyValue.getValue());
470 			}
471 			for (SectionEntryComment comment : section.comments) {
472 				addComment(comment.getText());				
473 			}
474 			return this;
475 		}
476 
477 		/**
478 		 * Writes this section into the writer.
479 		 * @param writer
480 		 */
481 		public void output(PrintWriter writer) {			
482 			List<SectionEntry> output = new ArrayList<SectionEntry>(props.values());
483 			
484 			List<SectionEntryComment> commentsWithKeys = new ArrayList<SectionEntryComment>(comments);
485 			List<SectionEntryComment> commentsWithoutKeys = new ArrayList<SectionEntryComment>();
486 			Iterator<SectionEntryComment> iter = commentsWithKeys.iterator();
487 			while (iter.hasNext()) {
488 				SectionEntryComment comment = iter.next();
489 				if (comment.getKey() == null || comment.getKey().length() <= 0) {
490 					commentsWithoutKeys.add(comment);
491 					iter.remove();
492 				} else {
493 					output.add(comment);
494 				}
495 			}
496 			
497 			Collections.sort(output, SECTION_ENTRY_KEY_COMPARATOR);
498 			
499 			mergeInComments(output, commentsWithoutKeys);
500 			
501 			writer.print("[");
502 			writer.print(name);
503 			writer.println("]");
504 			for (SectionEntry entry : output) {
505 				writer.println(entry.getIniFileLines());
506 			}
507 		}
508 		
509 		private void mergeInComments(List<SectionEntry> output,	List<SectionEntryComment> commentsWithKeys) {
510 			if (output.size() == 0) {
511 				output.addAll(commentsWithKeys);
512 				Collections.sort(output, SECTION_ENTRY_INDEX_COMPARATOR);
513 				return;
514 			}
515 			while(commentsWithKeys.size() > 0) {
516 				Iterator<SectionEntryComment> iter = commentsWithKeys.iterator();
517 				while (iter.hasNext()) {
518 					SectionEntryComment comment = iter.next();
519 					if (mergeInComment(output, comment)) {
520 						iter.remove();
521 					}
522 				}
523 			}			
524 		}
525 
526 		private boolean mergeInComment(List<SectionEntry> output, SectionEntryComment comment) {
527 			if (output.size() == 0) {
528 				output.add(comment);
529 				return true;
530 			}
531 			for (int i = 0; i < output.size(); ++i) {
532 				SectionEntry entry = output.get(i);
533 				if (entry.getSectionEntryIndex()-1 == comment.getSectionEntryIndex()) {
534 					output.add(i, comment);
535 					return true;
536 				}
537 			}
538 			if (comment.getSectionEntryIndex()-1 == output.get(output.size()-1).getSectionEntryIndex()) {
539 				output.add(comment);
540 				return true;
541 			}
542 			return false;
543 		}
544 
545 		@Override
546 		public String toString() {
547 			return "IniFile.Section[name=" + name + ", entries=" + props.size() + "]";
548 		}
549 		
550 	}
551 	
552 	private Map<String, Section> sections = new TreeMap<String, Section>(new Comparator<String>() {
553 		@Override
554 		public int compare(String o1, String o2) {
555 			if (o1 == null) return -1;
556 			if (o2 == null) return 1;
557 			return o1.toLowerCase().compareTo(o2.toLowerCase());
558 		}
559 	});
560 	
561 	/**
562 	 * Constructs Ini file with no defaults.
563 	 */
564 	public IniFile() {
565 	}
566 	
567 	/**
568 	 * Initialize object with defaults taken from 'source' (file must exists!).
569 	 * 
570 	 * @param source
571 	 */
572 	public IniFile(File source) {
573 		if (!source.exists()) {
574 			throw new PogamutException("File with defaults does not exist at: " + source.getAbsolutePath() + ".", this);
575 		}
576 		load(source);
577 	}
578 	
579 	/**
580 	 * Initialize object with defaults taken from 'source' (file must exists!).
581 	 * 
582 	 * @param source
583 	 */
584 	public IniFile(InputStream source) {
585 		load(source);
586 	}
587 		
588 	/**
589 	 * Copy-constructor. 
590 	 *
591 	 * @param ini
592 	 */
593 	public IniFile(IniFile ini) {
594 		for (Section section : ini.getSections()) {
595 			addSection(new Section(section));
596 		}
597 	}
598 
599 	/**
600 	 * Loads {@link IniFile#source} into {@link IniFile#sections}.
601 	 * <p><p>
602 	 * Note that his method won't clear anything, it will just load all sections/properties from the given file possibly overwriting existing properties
603 	 * in existing sections.
604 	 * 
605 	 * @param source 
606 	 */
607 	public void load(File source) {
608 		try {
609 			load(new FileInputStream(source));
610 		} catch (Exception e) {
611 			throw new PogamutException("Could not load defaults for GameBots2004.ini from file: " + source.getAbsolutePath() + ", caused by: " + e.getMessage(), e);
612 		}
613 	}
614 	
615 	/**
616 	 * Loads {@link IniFile#source} into {@link IniFile#sections}.
617 	 * <p><p>
618 	 * Note that his method won't clear anything, it will just load all sections/properties from the given file possibly overwriting existing properties
619 	 * in existing sections.
620 	 * 
621 	 * @param source 
622 	 */
623 	public void load(InputStream source) {
624 		BufferedReader reader = null;
625 		reader = new BufferedReader(new InputStreamReader(source));
626 		
627 		Section currSection = null;
628 		String currComment = null;
629 		
630 		try {
631 			while (reader.ready()) {
632 				String line = reader.readLine().trim();
633 				
634 				if (line.length() == 0) {
635 					continue;
636 				}
637 				
638 				if (isComment(line)) {
639 					if (currSection == null) {
640 						if (currComment == null) currComment = line.trim();
641 						else currComment += "\n" + line;
642 					} else {
643 						if (hasCommentKey(line)) {
644 							currSection.addComment(line, currComment);
645 						} else {
646 							if (currComment == null) currComment = line.trim();
647 							else currComment += "\n" + line;
648 						}
649 					}					
650 					continue;
651 				}
652 				
653 				if (line.startsWith("[") && line.endsWith("]")) {
654 					if (currComment != null && currSection != null) {
655 						currSection.addComment(currComment);
656 					}
657 					if (currSection != null && getSection(currSection.getName()) == null) {
658 						addSection(currSection);
659 					}
660 					String sectionName = line.substring(1, line.length()-1);
661 					if (hasSection(sectionName)) {
662 						// ONE SECTION ENCOUNTERED TWICE WITHIN THE INI FILE...
663 						currSection = getSection(sectionName);
664 					} else {
665 						currSection = getSection(sectionName);
666 					}
667 					if (currSection == null) currSection = new Section(sectionName);
668 					continue;
669 				} 
670 				
671 				int separ = line.indexOf("=");
672 				if (separ < 0) {
673 					// TODO: [Jimmy] throw an exception? at least log somewhere?
674 					continue;
675 				}
676 				if (currSection == null) {
677 					throw new PogamutException("There is an entry '" + line + "' inside ini file that does not belong to any section.", this);
678 				}
679 				String key = line.substring(0, separ);					
680 				String value =
681 					(separ+1 < line.length() ? 
682 							line.substring(separ+1, line.length())
683 						:	"");
684 				currSection.add(key, value, currComment);
685 				currComment = null;
686 			}
687 		} catch (IOException e) {
688 			throw new PogamutIOException("Could not completely read file with defaults from stream, caused by: " + e.getMessage(), e, this);
689 		} finally {
690 			try {
691 				reader.close();
692 			} catch (IOException e) {
693 			}
694 		}
695 		
696 		if (currSection != null && getSection(currSection.getName()) == null) {
697 			addSection(currSection);
698 		}
699 	}
700 
701 	/**
702 	 * Add all sections from one ini file into this one.
703 	 * @param iniFile
704 	 * @return this
705 	 */
706 	public IniFile addIniFile(IniFile iniFile) {
707 		for (Section section : iniFile.sections.values()) {
708 			addSection(section);
709 		}
710 		return this;
711 	}
712 	
713 	/**
714 	 * Adds a new section into this class (won't overwrite existing one).
715 	 * 
716 	 * @param sectionName
717 	 * @return section that is stored in this class
718 	 */
719 	public Section addSection(String sectionName) {
720 		return addSection(new Section(sectionName));
721 	}
722 	
723 	/**
724 	 * Adds section into this ini file. If section of the same name exists, it won't be replaced. Instead all properties
725 	 * from 'section' will be put there.
726 	 * <p><p>
727 	 * If 'section' is a new section (i.e., its {@link Section#getName()} is not already present in stored sections), this instance
728 	 * will be stored here (does not hard-copy the section!). For hard-copy variant, use {@link IniFile#copySection(Section)}.
729 	 * 
730 	 * @param section
731 	 * @return section that is stored in this class
732 	 */
733 	public Section addSection(Section section) {
734 		Section oldSection = sections.get(section.getName());
735 		if (oldSection != null) {
736 			oldSection.add(section);
737 			return oldSection;
738 		} else {
739 			sections.put(section.getName(), section);
740 			return section;
741 		}
742 	}
743 	
744 	public Section copySection(Section section) {
745 		Section oldSection = sections.get(section.getName());
746 		if (oldSection != null) {
747 			oldSection.add(new Section(section));
748 			return oldSection;
749 		} else {
750 			sections.put(section.getName(), section);
751 			return section;
752 		}
753 	}
754 	
755 	public boolean hasSection(String name) {
756 		return sections.containsKey(name);
757 	}
758 	
759 	public Section getSection(String name) {
760 		return sections.get(name);
761 	}
762 	
763 	public Set<String> getSectionNames() {
764 		return sections.keySet();
765 	}
766 	
767 	public Collection<Section> getSections() {
768 		return sections.values();
769 	}
770 	
771 	public String getOne(String section, String key) {
772 		Section sec = sections.get(section);
773 		if (sec == null) return null;
774 		return sec.getOne(key);
775 	}
776 	
777 	public List<String> getAll(String section, String key) {
778 		Section sec = sections.get(section);
779 		if (sec == null) return null;
780 		return sec.getAll(key);
781 	}
782 	
783 	/**
784 	 * Sets property key=value into section 'section'
785 	 * @param section
786 	 * @param key
787 	 * @param value
788 	 * @return section instance that the property was set into
789 	 */
790 	public Section set(String section, String key, String value) {
791 		Section sec = sections.get(section);
792 		if (sec == null) {
793 			sec = addSection(section);
794 		}
795 		sec.set(key, value);
796 		return sec;
797 	}
798 	
799 	/**
800 	 * Set 'values' into this IniFile, alias for {@link IniFile#addIniFile(IniFile)}.
801 	 * @param values
802 	 * @return this
803 	 */
804 	public IniFile set(IniFile values) {
805 		return addIniFile(values);
806 	}
807 
808 	/**
809 	 * Set key=values from 'section' into this IniFile. Alias for {@link IniFile#addSection(Section)}.
810 	 * @param section
811 	 * @return section that is stored in this class
812 	 */
813 	public Section set(Section section) {
814 		return addSection(section);		
815 	}
816 
817 	/**
818 	 * Whether this line is comment. (Does not check for end lines).
819 	 * @param text
820 	 * @return
821 	 */
822 	public static boolean isComment(String line) {
823 		return line.trim().startsWith(";");
824 	}
825 	
826 	/**
827 	 * Whether this non-comment line is a key=value. (Does not check for end lines).
828 	 * @param keyValue
829 	 * @return
830 	 */
831 	public static  boolean hasKey(String keyValue) {
832 		if (isComment(keyValue)) return false;
833 		return keyValue.indexOf("=") > 0;
834 	}
835 	
836 	/**
837 	 * Whether this comment is commented key=value pair. (Does not check for end lines).
838 	 * @param comment
839 	 * @return
840 	 */
841 	public static boolean hasCommentKey(String comment) {
842 		if (!isComment(comment)) return false;
843 		while (comment.startsWith(";")) comment = comment.substring(1);
844 		int separ = comment.indexOf("=");
845 		if (separ <= 0) return false;
846 		String key = comment.substring(0, separ);
847 		return !key.contains(" ");
848 	}
849 	
850 	/**
851 	 * Similar to {@link #output(String)} but this ensure not to overwrite any file + it appends current "_date_time" to filename.
852 	 * 
853 	 * @param file
854 	 */
855 	public void backup(String pathToFileToBeCreated) {
856 		File file = new File(pathToFileToBeCreated);
857 		String fullpath = file.getAbsolutePath();
858 		String separator = System.getProperty("file.separator");
859 		String path = (fullpath.lastIndexOf(separator) >= 0 ? fullpath.substring(0, fullpath.lastIndexOf(separator)) : ".");
860 		String filename = (fullpath.lastIndexOf(separator) >= 0 ? fullpath.substring(fullpath.lastIndexOf(separator)+1) : "file.ini");
861 		String name = (filename.lastIndexOf(".") >= 0 ? filename.substring(0, filename.lastIndexOf(".")) : filename);
862 		String extension = (filename.lastIndexOf(".") >= 0 ? filename.substring(filename.lastIndexOf(".") + 1) : "ini");
863 		Date date = new Date(System.currentTimeMillis());
864 		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
865 		name += "." + sdf.format(date);
866 		
867 		File targetFile = null;
868 		int i = 0;
869 		
870 		while (true) {
871 			targetFile = new File(path + separator + name + (i > 0 ? "_" + i : "") + "." + extension);
872 			if (!targetFile.exists()) break; 
873 			++i;
874 		}
875 		
876 		output(targetFile);		
877 	}
878 	
879 	/**
880 	 * Outputs GameBots2004.ini stored by this class into 'file'. If 'file' exists, it overwrites it.
881 	 * 
882 	 * @param file
883 	 */
884 	public void output(String pathToFileToBeCreated) {
885 		NullCheck.check(pathToFileToBeCreated, "pathToFileToBeCreated");
886 		output(new File(pathToFileToBeCreated));
887 	}
888 	
889 	
890 	/**
891 	 * Outputs GameBots2004.ini stored by this class into 'file'. If 'file' exists, it overwrites it.
892 	 * 
893 	 * @param file
894 	 */
895 	public void output(File file) {
896 		NullCheck.check(file, "file");
897 		PrintWriter writer = null;
898 		try {
899 			writer = new PrintWriter(new FileWriter(file));
900 			output(writer);
901 		} catch (IOException e) {
902 			throw new PogamutIOException("Could not write ini file into '" + file.getAbsolutePath() + "', caused by: " + e.getMessage(), e, this);
903 		} finally {
904 			writer.close();
905 		}
906 	}
907 	
908 	/**
909 	 * Outputs contents of this {@link IniFile} into the 'writer'.
910 	 * 
911 	 * @param writer
912 	 */
913 	public void output(PrintWriter writer) {
914 		NullCheck.check(writer, "writer");
915 		boolean first = true;
916 		for (Section section : sections.values()) {
917 			if (first) first = false;
918 			else writer.println();
919 			section.output(writer);
920 		}
921 	}
922 	
923 	/**
924 	 * Returns contents of this {@link IniFile} as string.
925 	 * 
926 	 * @return
927 	 */
928 	public String output() {
929 		StringWriter stringWriter = new StringWriter();
930 		output(new PrintWriter(stringWriter));
931 		return stringWriter.toString();
932 	}
933 	
934 	//
935 	// MERGE INTO
936 	//
937 	
938 	public static void mergeIntoIniFile(IniFile values, File mergeIntoIniFile) {
939 		IniFile read = new IniFile(mergeIntoIniFile);
940 		if (read == null) {
941 			throw new RuntimeException("Failed to read ini file: " + mergeIntoIniFile.getAbsolutePath());
942 		}
943 		read.set(values);
944 		read.output(mergeIntoIniFile);
945 	}
946 	
947 	//
948 	// FOR TESTS
949 	// 
950 	
951 	/**
952 	 * Checks, whether THIS {@link IniFile} is the subset of 'other'.
953 	 * @param other
954 	 * @param thisName
955 	 * @param otherName
956 	 * @param log
957 	 * @return
958 	 */
959 	public boolean isSubset(IniFile other, String thisName, String otherName, Logger log) {
960 		for (Section thisSection : this.getSections()) {
961 			log.info("Checking section [" + thisSection.getName() + "]");
962 			Section otherSection = other.getSection(thisSection.getName());
963 			if (otherSection == null) {
964 				if (log != null) log.severe(thisName + " INI contains Section[" + thisSection.getName() + "] that is not present within Read INI (source)!");
965 				return false;
966 			}
967 			for (String key : thisSection.getKeys()) {
968 				List<String> thisValues = thisSection.getAll(key);
969 				List<String> otherValues = new ArrayList<String>(otherSection.getAll(key));
970 				if (log != null) log.info("Checking key: " + key + " (" + thisValues.size() + " values)");
971 				
972 				if (thisValues.size() != otherValues.size()) {
973 					if (log != null) log.severe(thisName + " INI, Section[" + thisSection.getName() + "], Key[" + key + "] contains #values == " + thisValues.size() + " != " + otherValues.size() + " == #values within " + otherName + " INI (source)!");
974 				}
975 				
976 				for (String testValue : thisValues) {
977 					boolean present = false;
978 					for (int i = 0; i < otherValues.size(); ++i) {
979 						String otherValue = otherValues.get(i);
980 						if (SafeEquals.equals(testValue, otherValue)) {
981 							present = true;
982 							otherValues.remove(i);
983 							break;
984 						}
985 					}
986 					if (present) continue;
987 					// ERROR!
988 					if (log != null) log.severe(thisName + " INI, Section[" + thisSection.getName() + "], Key[" + key + "] contains Value[" + testValue + "] that is not present within " + otherName + " section/key!");
989 					return false;
990 				}
991 			}			
992 		}
993 		return true;
994 	}
995 	
996 	/**
997 	 * Checks, whether THIS {@link IniFile} contains the same sections/keys/values as 'other'.
998 	 * @param other
999 	 * @param thisName
1000 	 * @param otherName
1001 	 * @param log
1002 	 * @return
1003 	 */
1004 	public boolean isEqual(IniFile other, String thisName, String otherName, Logger log) {
1005 		return isSubset(other, thisName, otherName, log) && other.isSubset(this, otherName, thisName, log);
1006 	}
1007 
1008 
1009 }