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 &#92;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    }