001 /**
002 *
003 * $Revision: 15169 $ $Date: 2009-05-11 14:05:40 +0200 (Mon, 11 May 2009) $
004 *
005 * This file is part of ** M y C o R e **
006 * Visit our homepage at http://www.mycore.de/ for details.
007 *
008 * This program is free software; you can use it, redistribute it
009 * and / or modify it under the terms of the GNU General Public License
010 * (GPL) as published by the Free Software Foundation; either version 2
011 * of the License or (at your option) any later version.
012 *
013 * This program is distributed in the hope that it will be useful, but
014 * WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program, normally in the file license.txt.
020 * If not, write to the Free Software Foundation Inc.,
021 * 59 Temple Place - Suite 330, Boston, MA 02111-1307 USA
022 *
023 **/
024 package org.mycore.common;
025
026 import java.io.BufferedReader;
027 import java.io.BufferedWriter;
028 import java.io.File;
029 import java.io.FileInputStream;
030 import java.io.FileNotFoundException;
031 import java.io.FileOutputStream;
032 import java.io.FileReader;
033 import java.io.IOException;
034 import java.io.InputStreamReader;
035 import java.io.OutputStreamWriter;
036 import java.io.UnsupportedEncodingException;
037 import java.nio.charset.Charset;
038 import java.util.ArrayList;
039 import java.util.Collections;
040 import java.util.Date;
041 import java.util.Enumeration;
042 import java.util.Iterator;
043 import java.util.List;
044 import java.util.Map;
045 import java.util.Properties;
046 import java.util.regex.Pattern;
047
048 import org.apache.tools.ant.BuildException;
049 import org.apache.tools.ant.Task;
050
051 /**
052 * Ant task that allows 'mycore.properties' manipulation via ant.
053 *
054 * @author Thomas Scheffler (yagee)
055 */
056 public class MCRConfigurationTask extends Task {
057 private static final Charset PROPERTY_CHARSET = Charset.forName("ISO-8859-1");
058
059 // some constants
060 private static final String MCR_CONFIGURATION_INCLUDE_DEFAULT = "MCR.Configuration.Include";
061
062 private Pattern includePattern;
063
064 // some fields setable and getable via methods
065 private String action;
066
067 private String key;
068
069 private String value;
070
071 private File propertyFile;
072
073 private File mergeFile;
074
075 // some fields needed for processing
076 private boolean valuePresent = false;
077
078 private boolean propertiesLoaded = false;
079
080 private boolean fileChanged = false;
081
082 private int lineNumber = -1;
083
084 private ArrayList<String> propLines;
085
086 /**
087 * Execute the requested operation.
088 *
089 * @throws BuildException
090 * if an error occurs
091 */
092 public void execute() throws BuildException {
093 checkPreConditions();
094 if (action != null && (action.equals("addInclude") || action.equals("removeInclude"))) {
095 loadLines();
096 if (!propertiesLoaded) {
097 throw new BuildException("Could not load: " + propertyFile.getName());
098 }
099 if (action.equals("addInclude")) {
100 addInclude();
101 } else if (action.equals("removeInclude")) {
102 removeInclude();
103 }
104 if (fileChanged) {
105 writeLines();
106 }
107 } else {
108 // new property merger starts here
109 try {
110 // super.log("Loading target property file: " + propertyFile);
111 Properties orig = getProperties(propertyFile);
112 super.log("Merging property file: " + mergeFile);
113 orig.putAll(getProperties(mergeFile));
114 // super.log("Saving target property file: " + propertyFile);
115 // remove any include statements
116 orig.remove("MCR.Configuration.Include");
117 AlphabeticallyPropertyOutputter.store(orig, propertyFile, "automatically generated by " + this.getClass().getName());
118 } catch (Exception e) {
119 throw new BuildException("Error while merging properties.", e);
120 }
121 }
122 reset();
123 }
124
125 private Properties getProperties(File pFile) throws FileNotFoundException, IOException {
126 Properties returns = new Properties();
127 if (pFile.exists())
128 returns.load(new FileInputStream(pFile));
129 return returns;
130 }
131
132 /**
133 * checks whether all preconditions are met
134 */
135 private void checkPreConditions() throws BuildException {
136 setIncludePattern(key);
137 if (action == null && (mergeFile == null)) {
138 throw new BuildException("Must specify 'action' attribute");
139 }
140 if (propertyFile == null) {
141 throw new BuildException("Must specify 'propertyfile' attribute");
142 }
143 if (value == null && (mergeFile == null)) {
144 throw new BuildException("Must specify 'value' attribute");
145 }
146 if ((mergeFile == null) && ((action == null) || (!action.equals("addInclude") && !action.equals("removeInclude")))) {
147 throw new BuildException("action must be either 'addInclude' or 'removeInclude'");
148 }
149 if (!propertyFile.exists() && (mergeFile == null)) {
150 throw new BuildException(new FileNotFoundException(propertyFile + " does not exists."));
151 }
152 if (mergeFile != null && !mergeFile.exists()) {
153 throw new BuildException(new FileNotFoundException(mergeFile + "does not exists."));
154 }
155 }
156
157 /**
158 * resets all local fields
159 */
160 private void reset() {
161 action = null;
162 key = null;
163 propertyFile = null;
164 mergeFile = null;
165 lineNumber = -1;
166 propertiesLoaded = false;
167 fileChanged = false;
168 valuePresent = false;
169 }
170
171 /**
172 * adds an include
173 */
174 private void addInclude() {
175 if (valuePresent) {
176 handleOutput(new StringBuffer("Not changing ").append(propertyFile.getName()).append(": '").append(value).append("' already included.").toString());
177 return;
178 }
179 fileChanged = true;
180 String prop = propLines.get(lineNumber).toString();
181 String newProp = prop;
182 if(prop.charAt(prop.length() - 1) != '=') {
183 newProp += ",";
184 }
185 newProp += value;
186 propLines.remove(lineNumber);
187 propLines.add(lineNumber, newProp);
188 handleOutput(new StringBuffer(propertyFile.getName()).append(':').append(lineNumber).append(" added '").append(value).append("' to ").append(getKey())
189 .toString());
190 }
191
192 /**
193 * removes an include
194 */
195 private void removeInclude() {
196 if (!valuePresent) {
197 handleOutput(new StringBuffer("Not changing ").append(propertyFile.getName()).append(": '").append(value).append("' not present.").toString());
198 return;
199 }
200 fileChanged = true;
201 String newProp = propLines.get(lineNumber).toString().replaceAll("," + value, "");
202 propLines.remove(lineNumber);
203 propLines.add(lineNumber, newProp);
204 handleOutput(new StringBuffer(propertyFile.getName()).append(':').append(lineNumber).append(" removed '").append(value).append("' from ").append(
205 getKey()).toString());
206 }
207
208 /*
209 * writes back the property file together with changed properties
210 */
211 private void writeLines() {
212 BufferedWriter writer = null;
213 try {
214 writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(propertyFile), PROPERTY_CHARSET));
215 for (Iterator it = propLines.iterator(); it.hasNext();) {
216 writer.write(it.next().toString());
217 writer.newLine();
218 }
219 } catch (IOException e) {
220 handleErrorOutput("Error while writing '" + propertyFile.getName() + "': " + e.getMessage());
221 } finally {
222 if (writer != null) {
223 try {
224 writer.close();
225 } catch (IOException e) {
226 handleErrorOutput("Error while closing file '" + propertyFile.getName() + "': " + e.getMessage());
227 }
228 }
229 }
230 }
231
232 /*
233 * loads the property file and marks the occurence of
234 * MCR.Configuration.Include
235 */
236 private void loadLines() {
237 BufferedReader reader = null;
238 propertiesLoaded = false;
239 try {
240 reader = new BufferedReader(new InputStreamReader(new FileInputStream(propertyFile), PROPERTY_CHARSET));
241 propLines = new ArrayList<String>(1000);
242 int i = 0;
243 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
244 // add each line of the property file to the array list
245 propLines.add(line);
246 if ((lineNumber < 0) && includePattern.matcher(line).find()) {
247 // found the MCR.Configuration.Include line
248 lineNumber = i;
249 if (line.indexOf(value) > 0) {
250 // value is included
251 valuePresent = true;
252 }
253 }
254 i++;
255 }
256 if (lineNumber < 0) {
257 propLines.add(key + "=");
258 lineNumber = propLines.size() - 1;
259 }
260 propertiesLoaded = true;
261 } catch (IOException e) {
262 handleErrorOutput("Error while reading '" + propertyFile.getName() + "': " + e.getMessage());
263 } finally {
264 try {
265 if (reader != null) {
266 reader.close();
267 }
268 } catch (IOException e) {
269 handleErrorOutput("Error while closing file '" + propertyFile.getName() + "': " + e.getMessage());
270 }
271 }
272 }
273
274 public String getAction() {
275 return action;
276 }
277
278 /**
279 * sets the action the task should perform.
280 *
281 * @param action
282 * either "addInclude" or "removeInclude"
283 */
284 public void setAction(String action) {
285 this.action = action;
286 }
287
288 public File getPropertyFile() {
289 return propertyFile;
290 }
291
292 /**
293 * sets the property file that needs to be changed.
294 *
295 * @param action
296 * a 'mycore.properties' file
297 */
298 public void setPropertyFile(File propertyFile) {
299 this.propertyFile = propertyFile;
300 }
301
302 public File getMergeFile() {
303 return mergeFile;
304 }
305
306 public void setMergeFile(File mergeFile) {
307 this.mergeFile = mergeFile;
308 }
309
310 public String getValue() {
311 return value;
312 }
313
314 /**
315 * sets the value for the action to be performed. For 'addInclude' a value
316 * of "mycore.properties.moduleXY" would result in adding
317 * ",mycore.properties.moduleXY" to the property
318 * "MCR.Configuration.Include".
319 *
320 * @param action
321 * a 'mycore.properties' file
322 */
323 public void setValue(String value) {
324 this.value = value;
325 }
326
327 public String getKey() {
328 if (key == null)
329 return MCR_CONFIGURATION_INCLUDE_DEFAULT;
330 else
331 return key;
332 }
333
334 public void setKey(String key) {
335 this.key = key;
336 }
337
338 public void setIncludePattern(String key) {
339 if (key == null)
340 this.includePattern = Pattern.compile(MCR_CONFIGURATION_INCLUDE_DEFAULT);
341 else
342 this.includePattern = Pattern.compile(key);
343 }
344
345 private static class AlphabeticallyPropertyOutputter {
346 public static void store(Properties properties, File file, String comments) throws IOException {
347 try {
348 store(properties, new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "8859_1")), comments);
349 } catch (FileNotFoundException e) {
350 // TODO Auto-generated catch block
351 e.printStackTrace();
352 }
353 }
354
355 private static void store(Properties properties, BufferedWriter bw, String comments) throws IOException {
356 boolean escUnicode = true;
357 if (comments != null) {
358 writeComments(bw, comments);
359 }
360 bw.write("#" + new Date().toString());
361 bw.newLine();
362 synchronized (properties) {
363 List<String> list = new ArrayList<String>(properties.size());
364 for (Object key : properties.keySet()) {
365 list.add(key.toString());
366 }
367 Collections.sort(list);
368 for (String key : list) {
369 String val = (String) properties.get(key);
370 key = saveConvert(key, true, escUnicode);
371 /*
372 * No need to escape embedded and trailing spaces for value,
373 * hence pass false to flag.
374 */
375 val = saveConvert(val, false, escUnicode);
376 bw.write(key + "=" + val);
377 bw.newLine();
378 }
379 }
380 bw.flush();
381 }
382
383 private static void writeComments(BufferedWriter bw, String comments) throws IOException {
384 bw.write("#");
385 int len = comments.length();
386 int current = 0;
387 int last = 0;
388 char[] uu = new char[6];
389 uu[0] = '\\';
390 uu[1] = 'u';
391 while (current < len) {
392 char c = comments.charAt(current);
393 if (c > '\u00ff' || c == '\n' || c == '\r') {
394 if (last != current)
395 bw.write(comments.substring(last, current));
396 if (c > '\u00ff') {
397 uu[2] = toHex((c >> 12) & 0xf);
398 uu[3] = toHex((c >> 8) & 0xf);
399 uu[4] = toHex((c >> 4) & 0xf);
400 uu[5] = toHex(c & 0xf);
401 bw.write(new String(uu));
402 } else {
403 bw.newLine();
404 if (c == '\r' && current != len - 1 && comments.charAt(current + 1) == '\n') {
405 current++;
406 }
407 if (current == len - 1 || (comments.charAt(current + 1) != '#' && comments.charAt(current + 1) != '!'))
408 bw.write("#");
409 }
410 last = current + 1;
411 }
412 current++;
413 }
414 if (last != current)
415 bw.write(comments.substring(last, current));
416 bw.newLine();
417 }
418
419 /*
420 * Converts unicodes to encoded \uxxxx and escapes special
421 * characters with a preceding slash
422 */
423 private static String saveConvert(String theString, boolean escapeSpace, boolean escapeUnicode) {
424 int len = theString.length();
425 int bufLen = len * 2;
426 if (bufLen < 0) {
427 bufLen = Integer.MAX_VALUE;
428 }
429 StringBuffer outBuffer = new StringBuffer(bufLen);
430
431 for (int x = 0; x < len; x++) {
432 char aChar = theString.charAt(x);
433 // Handle common case first, selecting largest block that
434 // avoids the specials below
435 if ((aChar > 61) && (aChar < 127)) {
436 if (aChar == '\\') {
437 outBuffer.append('\\');
438 outBuffer.append('\\');
439 continue;
440 }
441 outBuffer.append(aChar);
442 continue;
443 }
444 switch (aChar) {
445 case ' ':
446 if (x == 0 || escapeSpace)
447 outBuffer.append('\\');
448 outBuffer.append(' ');
449 break;
450 case '\t':
451 outBuffer.append('\\');
452 outBuffer.append('t');
453 break;
454 case '\n':
455 outBuffer.append('\\');
456 outBuffer.append('n');
457 break;
458 case '\r':
459 outBuffer.append('\\');
460 outBuffer.append('r');
461 break;
462 case '\f':
463 outBuffer.append('\\');
464 outBuffer.append('f');
465 break;
466 case '=': // Fall through
467 case ':': // Fall through
468 case '#': // Fall through
469 case '!':
470 outBuffer.append('\\');
471 outBuffer.append(aChar);
472 break;
473 default:
474 if (((aChar < 0x0020) || (aChar > 0x007e)) & escapeUnicode) {
475 outBuffer.append('\\');
476 outBuffer.append('u');
477 outBuffer.append(toHex((aChar >> 12) & 0xF));
478 outBuffer.append(toHex((aChar >> 8) & 0xF));
479 outBuffer.append(toHex((aChar >> 4) & 0xF));
480 outBuffer.append(toHex(aChar & 0xF));
481 } else {
482 outBuffer.append(aChar);
483 }
484 }
485 }
486 return outBuffer.toString();
487 }
488
489 /**
490 * Convert a nibble to a hex character
491 *
492 * @param nibble
493 * the nibble to convert.
494 */
495 private static char toHex(int nibble) {
496 return hexDigit[(nibble & 0xF)];
497 }
498
499 /** A table of hex digits */
500 private static final char[] hexDigit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
501
502 }
503
504 }