    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}