001/*
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.   
018 */
019package org.apache.wiki.util;
020
021import org.apache.log4j.Logger;
022
023import java.io.BufferedReader;
024import java.io.ByteArrayOutputStream;
025import java.io.File;
026import java.io.FileOutputStream;
027import java.io.IOException;
028import java.io.InputStream;
029import java.io.InputStreamReader;
030import java.io.OutputStream;
031import java.io.OutputStreamWriter;
032import java.io.Reader;
033import java.io.StringReader;
034import java.io.StringWriter;
035import java.io.Writer;
036import java.nio.ByteBuffer;
037import java.nio.CharBuffer;
038import java.nio.charset.CharacterCodingException;
039import java.nio.charset.Charset;
040import java.nio.charset.CharsetDecoder;
041import java.nio.charset.CodingErrorAction;
042import java.nio.charset.StandardCharsets;
043
044/**
045 *  Generic utilities related to file and stream handling.
046 */
047public final class FileUtil {
048
049    /** Size of the buffer used when copying large chunks of data. */
050    private static final int      BUFFER_SIZE = 4096;
051    private static final Logger   log         = Logger.getLogger(FileUtil.class);
052
053    /**
054     *  Private constructor prevents instantiation.
055     */
056    private FileUtil()
057    {}
058
059    /**
060     *  Makes a new temporary file and writes content into it. The temporary
061     *  file is created using <code>File.createTempFile()</code>, and the usual
062     *  semantics apply.  The files are not deleted automatically in exit.
063     *
064     *  @param content Initial content of the temporary file.
065     *  @param encoding Encoding to use.
066     *  @return The handle to the new temporary file
067     *  @throws IOException If the content creation failed.
068     *  @see java.io.File#createTempFile(String,String,File)
069     */
070    public static File newTmpFile( final String content, final Charset encoding ) throws IOException {
071        final File f = File.createTempFile( "jspwiki", null );
072        try( final Reader in = new StringReader( content );
073             final Writer out = new OutputStreamWriter( new FileOutputStream( f ), encoding ) ) {
074            copyContents( in, out );
075        }
076
077        return f;
078    }
079
080    /**
081     *  Creates a new temporary file using the default encoding of ISO-8859-1 (Latin1).
082     *
083     *  @param content The content to put into the file.
084     *  @throws IOException If writing was unsuccessful.
085     *  @return A handle to the newly created file.
086     *  @see #newTmpFile( String, Charset )
087     *  @see java.io.File#createTempFile(String,String,File)
088     */
089    public static File newTmpFile( final String content ) throws IOException {
090        return newTmpFile( content, StandardCharsets.ISO_8859_1 );
091    }
092
093    /**
094     *  Runs a simple command in given directory. The environment is inherited from the parent process (e.g. the
095     *  one in which this Java VM runs).
096     *
097     *  @return Standard output from the command.
098     *  @param  command The command to run
099     *  @param  directory The working directory to run the command in
100     *  @throws IOException If the command failed
101     *  @throws InterruptedException If the command was halted
102     */
103    public static String runSimpleCommand( final String command, final String directory ) throws IOException, InterruptedException {
104        log.info( "Running simple command " + command + " in " + directory );
105        final StringBuilder result = new StringBuilder();
106        final Process process = Runtime.getRuntime().exec( command, null, new File( directory ) );
107
108        try( final BufferedReader stdout = new BufferedReader( new InputStreamReader( process.getInputStream() ) );
109             final BufferedReader stderr = new BufferedReader( new InputStreamReader( process.getErrorStream() ) ) ) {
110            String line;
111
112            while( (line = stdout.readLine()) != null ) {
113                result.append( line+"\n");
114            }
115
116            final StringBuilder error = new StringBuilder();
117            while( (line = stderr.readLine()) != null ) {
118                error.append( line+"\n");
119            }
120
121            if( error.length() > 0 ) {
122                log.error("Command failed, error stream is: "+error);
123            }
124
125            process.waitFor();
126        } finally {
127            // we must close all by exec(..) opened streams: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4784692
128            process.getInputStream().close();
129        }
130
131        return result.toString();
132    }
133
134
135    /**
136     *  Just copies all characters from <I>in</I> to <I>out</I>.  The copying is performed using a buffer of bytes.
137     *
138     *  @since 1.5.8
139     *  @param in The reader to copy from
140     *  @param out The reader to copy to
141     *  @throws IOException If reading or writing failed.
142     */
143    public static void copyContents( final Reader in, final Writer out ) throws IOException {
144        char[] buf = new char[BUFFER_SIZE];
145        int bytesRead;
146        while( ( bytesRead = in.read( buf ) ) > 0 ) {
147            out.write( buf, 0, bytesRead );
148        }
149
150        out.flush();
151    }
152
153    /**
154     *  Just copies all bytes from <I>in</I> to <I>out</I>.  The copying is performed using a buffer of bytes.
155     *
156     *  @since 1.9.31
157     *  @param in The inputstream to copy from
158     *  @param out The outputstream to copy to
159     *  @throws IOException In case reading or writing fails.
160     */
161    public static void copyContents( final InputStream in, final OutputStream out ) throws IOException {
162        byte[] buf = new byte[BUFFER_SIZE];
163        int bytesRead;
164        while( ( bytesRead = in.read( buf ) ) > 0 ) {
165            out.write( buf, 0, bytesRead );
166        }
167
168        out.flush();
169    }
170
171    /**
172     *  Reads in file contents.
173     *  <P>
174     *  This method is smart and falls back to ISO-8859-1 if the input stream does not
175     *  seem to be in the specified encoding.
176     *
177     *  @param input The InputStream to read from.
178     *  @param encoding The encoding to assume at first.
179     *  @return A String, interpreted in the "encoding", or, if it fails, in Latin1.
180     *  @throws IOException If the stream cannot be read or the stream cannot be
181     *          decoded (even) in Latin1
182     */
183    public static String readContents( InputStream input, String encoding )
184        throws IOException
185    {
186        ByteArrayOutputStream out = new ByteArrayOutputStream();
187        FileUtil.copyContents( input, out );
188
189        ByteBuffer     bbuf        = ByteBuffer.wrap( out.toByteArray() );
190
191        Charset        cset        = Charset.forName( encoding );
192        CharsetDecoder csetdecoder = cset.newDecoder();
193
194        csetdecoder.onMalformedInput( CodingErrorAction.REPORT );
195        csetdecoder.onUnmappableCharacter( CodingErrorAction.REPORT );
196
197        try
198        {
199            CharBuffer cbuf = csetdecoder.decode( bbuf );
200
201            return cbuf.toString();
202        }
203        catch( CharacterCodingException e )
204        {
205            Charset        latin1    = Charset.forName("ISO-8859-1");
206            CharsetDecoder l1decoder = latin1.newDecoder();
207
208            l1decoder.onMalformedInput( CodingErrorAction.REPORT );
209            l1decoder.onUnmappableCharacter( CodingErrorAction.REPORT );
210
211            try
212            {
213                bbuf = ByteBuffer.wrap( out.toByteArray() );
214
215                CharBuffer cbuf = l1decoder.decode( bbuf );
216
217                return cbuf.toString();
218            }
219            catch( CharacterCodingException ex )
220            {
221                throw (CharacterCodingException) ex.fillInStackTrace();
222            }
223        }
224    }
225
226    /**
227     *  Returns the full contents of the Reader as a String.
228     *
229     *  @since 1.5.8
230     *  @param in The reader from which the contents shall be read.
231     *  @return String read from the Reader
232     *  @throws IOException If reading fails.
233     */
234    public static String readContents( Reader in )
235        throws IOException
236    {
237        Writer out = null;
238
239        try
240        {
241            out = new StringWriter();
242
243            copyContents( in, out );
244
245            return out.toString();
246        }
247        finally
248        {
249            try
250            {
251                out.close();
252            }
253            catch( Exception e )
254            {
255                log.error("Not able to close the stream while reading contents.");
256            }
257        }
258    }
259
260    /**
261     *  Returns the class and method name (+a line number) in which the
262     *  Throwable was thrown.
263     *
264     *  @param t A Throwable to analyze.
265     *  @return A human-readable string stating the class and method.  Do not rely
266     *          the format to be anything fixed.
267     */
268    public static String getThrowingMethod( Throwable t )
269    {
270        StackTraceElement[] trace = t.getStackTrace();
271        StringBuilder sb = new StringBuilder();
272
273        if( trace == null || trace.length == 0 )
274        {
275            sb.append( "[Stack trace not available]" );
276        }
277        else
278        {
279            sb.append( trace[0].isNativeMethod() ? "native method" : "" );
280            sb.append( trace[0].getClassName() );
281            sb.append(".");
282            sb.append(trace[0].getMethodName()+"(), line "+trace[0].getLineNumber());
283        }
284        return sb.toString();
285    }
286}