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