Add MapWriter.
[jSite2.git] / src / net / pterodactylus / util / collection / MapWriter.java
1 /*
2  * jSite-next - MapWriter.java -
3  * Copyright © 2008 David Roden
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18  */
19
20 package net.pterodactylus.util.collection;
21
22 import java.io.BufferedReader;
23 import java.io.IOException;
24 import java.io.Reader;
25 import java.io.Writer;
26 import java.util.HashMap;
27 import java.util.Map;
28 import java.util.Properties;
29 import java.util.Map.Entry;
30 import java.util.logging.Level;
31 import java.util.logging.Logger;
32
33 import net.pterodactylus.util.io.Closer;
34 import net.pterodactylus.util.logging.Logging;
35 import net.pterodactylus.util.number.Hex;
36
37 /**
38  * Helper class that emulates the function of
39  * {@link Properties#store(java.io.OutputStream, String)} and
40  * {@link Properties#load(java.io.InputStream)} but does not suffer from the
41  * drawbacks of {@link Properties} (namely the fact that a
42  * <code>Properties</code> can not contain <code>null</code> values).
43  *
44  * @author David Roden &lt;droden@gmail.com&gt;
45  */
46 public class MapWriter {
47
48         /** The logger. */
49         private static final Logger logger = Logging.getLogger(MapWriter.class.getName());
50
51         /**
52          * Writes the given map to the given writer.
53          *
54          * @param writer
55          *            The writer to write the map’s content to
56          * @param map
57          *            The map to write
58          * @throws IOException
59          *             if an I/O error occurs
60          */
61         public static void write(Writer writer, Map<String, String> map) throws IOException {
62                 for (Entry<String, String> entry : map.entrySet()) {
63                         if (entry.getValue() != null) {
64                                 writer.write(encode(entry.getKey()));
65                                 writer.write('=');
66                                 writer.write(encode(entry.getValue()));
67                                 writer.write('\n');
68                         }
69                 }
70         }
71
72         /**
73          * Reads a map from the given reader. Lines are read from the given reader
74          * until a line is encountered that does not contain a colon (“:”) or equals
75          * sign (“=”).
76          *
77          * @param reader
78          *            The reader to read from
79          * @return The map that was read
80          * @throws IOException
81          *             if an I/O error occurs
82          */
83         public static Map<String, String> read(Reader reader) throws IOException {
84                 logger.log(Level.FINE, "MapWriter.read(reader=" + reader + ")");
85                 Map<String, String> map = new HashMap<String, String>();
86                 BufferedReader bufferedReader = new BufferedReader(reader);
87                 try {
88                         String line;
89                         while ((line = bufferedReader.readLine()) != null) {
90                                 logger.log(Level.FINEST, "Read line: “" + line + "”");
91                                 if (line.startsWith("#") || (line.length() == 0)) {
92                                         continue;
93                                 }
94                                 if (line.indexOf('=') == -1) {
95                                         break;
96                                 }
97                                 int split = line.indexOf('=');
98                                 String key = decode(line.substring(0, split));
99                                 String value = decode(line.substring(split + 1));
100                                 map.put(key, value);
101                         }
102                 } finally {
103                         Closer.close(bufferedReader);
104                 }
105                 return map;
106         }
107
108         //
109         // PRIVATE METHODS
110         //
111
112         /**
113          * Encodes the given String by replacing certain “unsafe” characters. CR
114          * (0x0d) is replaced by “\r”, LF (0x0a) is replaced by “\n”, the backslash
115          * (‘\’) will be replaced by “\\”, other characters that are either smaller
116          * than 0x20 or larger than 0x7f or that are ‘:’ or ‘=’ will be replaced by
117          * their unicode notation (“\u0000” for NUL, 0x00). All other values are
118          * copied verbatim.
119          *
120          * @param value
121          *            The value to encode
122          * @return The encoded value
123          */
124         static String encode(String value) {
125                 StringBuilder encodedString = new StringBuilder();
126                 for (char character : value.toCharArray()) {
127                         if (character == 0x0d) {
128                                 encodedString.append("\\r");
129                         } else if (character == 0x0a) {
130                                 encodedString.append("\\n");
131                         } else if (character == '\\') {
132                                 encodedString.append("\\\\");
133                         } else if ((character < 0x20) || (character == '=') || (character > 0x7f)) {
134                                 encodedString.append("\\u").append(Hex.toHex(character, 4));
135                         } else {
136                                 encodedString.append(character);
137                         }
138                 }
139                 return encodedString.toString();
140         }
141
142         /**
143          * Decodes the given value by reversing the changes made by
144          * {@link #encode(String)}.
145          *
146          * @param value
147          *            The value to decode
148          * @return The decoded value
149          */
150         static String decode(String value) {
151                 StringBuilder decodedString = new StringBuilder();
152                 boolean backslash = false;
153                 int hexDigit = 0;
154                 char[] hexDigits = new char[4];
155                 for (char character : value.toCharArray()) {
156                         if (hexDigit > 0) {
157                                 hexDigits[hexDigit - 1] = character;
158                                 hexDigit++;
159                                 if (hexDigit > 4) {
160                                         decodedString.append((char) Integer.parseInt(new String(hexDigits), 16));
161                                         hexDigit = 0;
162                                 }
163                         } else if (backslash) {
164                                 if (character == '\\') {
165                                         decodedString.append('\\');
166                                 } else if (character == 'r') {
167                                         decodedString.append('\r');
168                                 } else if (character == 'n') {
169                                         decodedString.append('\n');
170                                 } else if (character == 'u') {
171                                         hexDigit = 1;
172                                 }
173                                 backslash = false;
174                         } else if (character == '\\') {
175                                 backslash = true;
176                                 continue;
177                         } else {
178                                 decodedString.append(character);
179                         }
180                 }
181                 return decodedString.toString();
182         }
183
184 }