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