Modified the video compression fixing bug with ffmpeg and elimating the swingworker

This commit is contained in:
Kelly Kelly 2022-05-25 15:48:38 -04:00
parent 918a7ac083
commit a952923233
2 changed files with 181 additions and 279 deletions

View File

@ -1,180 +0,0 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2022 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.coreutils;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import javax.swing.SwingWorker;
import org.opencv.core.Mat;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;
import org.opencv.videoio.VideoCapture;
import org.opencv.videoio.VideoWriter;
import org.opencv.videoio.Videoio;
import static org.sleuthkit.autopsy.coreutils.VideoUtils.isVideoThumbnailSupported;
import org.sleuthkit.autopsy.datamodel.ContentUtils;
import org.sleuthkit.datamodel.AbstractFile;
/**
* A SwingWorker that will create a video of screen captures.
*
* To have the the gui update with process add a PropertyChangeListener for
* the property 'progress'.
*
* To update the gui on completion subclass this class and implement
* the SwingWorker done method.
*
*/
public class VideoSnapShotWorker extends SwingWorker<Void, Void>{
private static final long MIN_FRAME_INTERVAL_MILLIS = 500;
private static final int DEFAULT_FRAMES_PER_SECOND = 1;
private static final int DEFAULT_TOTAL_FRAMES = 100;
private static final int DEFAULT_SCALE = 100;
private final AbstractFile abstractFile;
private final File outputFile;
private final long numFrames;
private final double scale;
private final int framesPerSecond;
/**
* Creates a new instance of the SwingWorker using the default parameters.
*
* @param abstractFile The path to the existing video.
* @param outputFile The output path of the snapshot video.
*/
public VideoSnapShotWorker(AbstractFile abstractFile, File outputFile) {
this(abstractFile, outputFile, DEFAULT_TOTAL_FRAMES, DEFAULT_SCALE, DEFAULT_FRAMES_PER_SECOND);
}
/**
* Creates a new instance of the SwingWorker.
*
* @param abstractFile The path to the existing video.
* @param outputFile The output path of the snapshot video.
* @param numFrames The number of screen captures to include in the video.
* @param scale % to scale from the original. Passing 0 or 100 will result in no change.
* @param framesPerSecond Effects how long each frame will appear in the video.
*/
VideoSnapShotWorker(AbstractFile abstractFile, File outputFile, long numFrames, double scale, int framesPerSecond) {
this.abstractFile = abstractFile;
this.outputFile = outputFile;
this.scale = scale;
this.numFrames = numFrames;
this.framesPerSecond = framesPerSecond;
}
public static boolean isSupported(AbstractFile file) {
return isVideoThumbnailSupported(file);
}
@Override
protected Void doInBackground() throws Exception {
outputFile.mkdirs();
if(outputFile.exists()) {
throw new IOException(String.format("Failed to compress %s, output file already exists %s", abstractFile.getName(), outputFile.toString()));
}
File inputFile = VideoUtils.getVideoFileInTempDir(abstractFile);
if (inputFile.exists() == false || inputFile.length() < abstractFile.getSize()) {
Files.createParentDirs(inputFile);
ContentUtils.writeToFile(abstractFile, inputFile, null, this, true);
}
String file_name = inputFile.toString();//OpenCV API requires string for file name
VideoCapture videoCapture = new VideoCapture(file_name); //VV will contain the videos
if (!videoCapture.isOpened()) //checks if file is not open
{
// add this file to the set of known bad ones
throw new Exception("Problem with video file; problem when attempting to open file.");
}
VideoWriter writer = null;
try {
// get the duration of the video
double fps = framesPerSecond == 0 ? videoCapture.get(Videoio.CAP_PROP_FPS) : framesPerSecond; // gets frame per second
double total_frames = videoCapture.get(7); // gets total frames
double milliseconds = 1000 * (total_frames / fps); //convert to ms duration
long myDurationMillis = (long) milliseconds;
if (myDurationMillis <= 0) {
throw new Exception(String.format("Failed to make snapshot video, original video has no duration. %s", inputFile.toString()));
}
// calculate the number of frames to capture
int numFramesToGet = (int) numFrames;
long frameInterval = myDurationMillis / numFrames;
if (frameInterval < MIN_FRAME_INTERVAL_MILLIS) {
numFramesToGet = 1;
}
// for each timeStamp, grab a frame
Mat mat = new Mat(); // holds image received from video in mat format (opencv format)
Size s = new Size((int) videoCapture.get(Videoio.CAP_PROP_FRAME_WIDTH), (int) videoCapture.get(Videoio.CAP_PROP_FRAME_HEIGHT));
Size newSize = (scale == 0 || scale == 100) ? s : new Size((int) (videoCapture.get(Videoio.CAP_PROP_FRAME_WIDTH) * scale), (int) (videoCapture.get(Videoio.CAP_PROP_FRAME_HEIGHT) * scale));
writer = new VideoWriter(outputFile.toString(), VideoWriter.fourcc('M', 'J', 'P', 'G'), 1, newSize, true);
if (!writer.isOpened()) {
throw new Exception(String.format("Problem with video file; problem when attempting to open output file. %s", outputFile.toString()));
}
for (int frame = 0; frame < numFramesToGet; ++frame) {
if(isCancelled()) {
break;
}
long timeStamp = frame * frameInterval;
videoCapture.set(0, timeStamp); //set video in timeStamp ms
if (!videoCapture.read(mat)) { // on Wav files, usually the last frame we try to read does not succeed.
continue;
}
Mat resized = new Mat();
Imgproc.resize(mat, resized, newSize, 0, 0, Imgproc.INTER_CUBIC);
writer.write(resized);
setProgress(numFramesToGet/frame);
}
} finally {
videoCapture.release();
if (writer != null) {
writer.release();
}
}
if(isCancelled()) {
if(outputFile.exists()) {
outputFile.delete();
}
}
return null;
}
}

View File

@ -20,8 +20,13 @@ package org.sleuthkit.autopsy.coreutils;
import com.google.common.io.Files; import com.google.common.io.Files;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Arrays; import java.util.Arrays;
@ -32,7 +37,11 @@ import java.util.TreeSet;
import java.util.logging.Level; import java.util.logging.Level;
import org.netbeans.api.progress.ProgressHandle; import org.netbeans.api.progress.ProgressHandle;
import org.opencv.core.Mat; import org.opencv.core.Mat;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;
import org.opencv.videoio.VideoCapture; import org.opencv.videoio.VideoCapture;
import org.opencv.videoio.VideoWriter;
import org.opencv.videoio.Videoio;
import org.openide.modules.InstalledFileLocator; import org.openide.modules.InstalledFileLocator;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.Case;
@ -46,7 +55,7 @@ import org.sleuthkit.datamodel.AbstractFile;
* *
*/ */
public class VideoUtils { public class VideoUtils {
private static final String FFMPEG = "ffmpeg"; private static final String FFMPEG = "ffmpeg";
private static final String FFMPEG_EXE = "ffmpeg.exe"; private static final String FFMPEG_EXE = "ffmpeg.exe";
@ -88,8 +97,8 @@ public class VideoUtils {
private static final int CV_CAP_PROP_POS_MSEC = 0; private static final int CV_CAP_PROP_POS_MSEC = 0;
private static final int CV_CAP_PROP_FRAME_COUNT = 7; private static final int CV_CAP_PROP_FRAME_COUNT = 7;
private static final int CV_CAP_PROP_FPS = 5; private static final int CV_CAP_PROP_FPS = 5;
private static final double[] FRAME_GRAB_POS_RATIO = { 0.50, 0.25, 0.75, 0.01 }; private static final double[] FRAME_GRAB_POS_RATIO = {0.50, 0.25, 0.75, 0.01};
static final Logger LOGGER = Logger.getLogger(VideoUtils.class.getName()); static final Logger LOGGER = Logger.getLogger(VideoUtils.class.getName());
@ -116,10 +125,10 @@ public class VideoUtils {
/** /**
* Generate a thumbnail for the supplied video. * Generate a thumbnail for the supplied video.
* *
* @param file The video file. * @param file The video file.
* @param iconSize The target icon size in pixels. * @param iconSize The target icon size in pixels.
* *
* @return The generated thumbnail. Can return null if an error occurred * @return The generated thumbnail. Can return null if an error occurred
* trying to generate the thumbnail, or if the current thread was * trying to generate the thumbnail, or if the current thread was
* interrupted. * interrupted.
@ -166,7 +175,7 @@ public class VideoUtils {
if (Thread.interrupted()) { if (Thread.interrupted()) {
return null; return null;
} }
double duration = 1000 * (totalFrames / fps); //total milliseconds double duration = 1000 * (totalFrames / fps); //total milliseconds
/* /*
@ -177,19 +186,18 @@ public class VideoUtils {
* be tried in a last-ditch effort, the idea being the video may be * be tried in a last-ditch effort, the idea being the video may be
* corrupt and that our best chance at retrieving a frame is early * corrupt and that our best chance at retrieving a frame is early
* on in the video. * on in the video.
* *
* If no frame can be retrieved, no thumbnail will be created. * If no frame can be retrieved, no thumbnail will be created.
*/ */
int[] framePositions = new int[] { int[] framePositions = new int[]{
(int) (duration * FRAME_GRAB_POS_RATIO[0]), (int) (duration * FRAME_GRAB_POS_RATIO[0]),
(int) (duration * FRAME_GRAB_POS_RATIO[1]), (int) (duration * FRAME_GRAB_POS_RATIO[1]),
(int) (duration * FRAME_GRAB_POS_RATIO[2]), (int) (duration * FRAME_GRAB_POS_RATIO[2]),
(int) (duration * FRAME_GRAB_POS_RATIO[3]), (int) (duration * FRAME_GRAB_POS_RATIO[3]),};
};
Mat imageMatrix = new Mat(); Mat imageMatrix = new Mat();
for (int i=0; i < framePositions.length; i++) { for (int i = 0; i < framePositions.length; i++) {
if (!videoFile.set(CV_CAP_PROP_POS_MSEC, framePositions[i])) { if (!videoFile.set(CV_CAP_PROP_POS_MSEC, framePositions[i])) {
LOGGER.log(Level.WARNING, "Error seeking to " + framePositions[i] + "ms in {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS LOGGER.log(Level.WARNING, "Error seeking to " + framePositions[i] + "ms in {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS
// If we can't set the time, continue to the next frame position and try again. // If we can't set the time, continue to the next frame position and try again.
@ -201,15 +209,15 @@ public class VideoUtils {
// If the image is bad for some reason, continue to the next frame position and try again. // If the image is bad for some reason, continue to the next frame position and try again.
continue; continue;
} }
break; break;
} }
// If the image is empty, return since no buffered image can be created. // If the image is empty, return since no buffered image can be created.
if (imageMatrix.empty()) { if (imageMatrix.empty()) {
return null; return null;
} }
int matrixColumns = imageMatrix.cols(); int matrixColumns = imageMatrix.cols();
int matrixRows = imageMatrix.rows(); int matrixRows = imageMatrix.rows();
@ -239,7 +247,7 @@ public class VideoUtils {
} }
return bufferedImage == null ? null : ScalrWrapper.resizeFast(bufferedImage, iconSize); return bufferedImage == null ? null : ScalrWrapper.resizeFast(bufferedImage, iconSize);
} }
public static boolean canCompressAndScale(AbstractFile file) { public static boolean canCompressAndScale(AbstractFile file) {
if (PlatformUtil.getOSName().toLowerCase().startsWith("windows")) { if (PlatformUtil.getOSName().toLowerCase().startsWith("windows")) {
@ -248,115 +256,84 @@ public class VideoUtils {
return false; return false;
} }
/** /**
* Compress the given the files. * Compress the given the files. This method takes advantage of the exiting
* getVideoFile method to create a temp copy of the inputFile. It does not
* delete the temp file, it leaves it in the video temp file for future use.
* *
* @param inputFile The AbstractFile representing the video. * When using this method there is no way to cancel the process between the
* @param outputFile Output file. * creation of the temp file and the launching of the process. For better
* @param terminator A processTerminator for the ffmpeg executable. * control use the other compressVideo method.
* @throws IOException *
* @param inputFile The AbstractFile representing the video.
* @param outputFile Output file.
* @param terminator A processTerminator for the ffmpeg executable.
* @param ffmpegStdout A file to send the output of ffmpeg stdout.
* @param ffmpegStderr A file to send the output of ffmpeg stderr.
*
* @return The ffmpeg process exit value.
*
* @throws IOException
*/ */
static public void compressVideo(AbstractFile inputFile, File outputFile, ExecUtil.ProcessTerminator terminator) throws IOException { static public int compressVideo(AbstractFile inputFile, File outputFile, ExecUtil.ProcessTerminator terminator, File ffmpegStdout, File ffmpegStderr) throws IOException {
compressVideo(getVideoFile(inputFile), outputFile, terminator); return compressVideo(getVideoFile(inputFile), outputFile, terminator, ffmpegStdout, ffmpegStderr);
} }
/** /**
* Compress the given the files. * Compress the given the files.
* *
* @param inputPath Absolute path to input video. * The output from ffmpeg seem to go to the error file not the
* @param outputPath Path for scaled file. * out file. Text in the err file does not mean an issue occurred.
*
* @param inputFile Absolute path to input video.
* @param outputFile Path for scaled file.
* @param terminator A processTerminator for the ffmpeg executable. * @param terminator A processTerminator for the ffmpeg executable.
* @throws Exception * @param ffmpegStdout A file to send the output of ffmpeg stdout.
* @param ffmpegStderr A file to send the output of ffmpeg stderr.
*
* @return The ffmpeg process exit value.
*
* @throws IOException
*/ */
static void compressVideo(File inputFile, File outputFile, ExecUtil.ProcessTerminator terminator) throws IOException { static public int compressVideo(File inputFile, File outputFile, ExecUtil.ProcessTerminator terminator, File ffmpegStdout, File ffmpegStderr) throws IOException {
Path executablePath = Paths.get(FFMPEG, FFMPEG_EXE); Path executablePath = Paths.get(FFMPEG, FFMPEG_EXE);
File exeFile = InstalledFileLocator.getDefault().locate(executablePath.toString(), VideoUtils.class.getPackage().getName(), true); File exeFile = InstalledFileLocator.getDefault().locate(executablePath.toString(), VideoUtils.class.getPackage().getName(), true);
if(exeFile == null) { if (exeFile == null) {
throw new IOException("Unable to compress ffmpeg.exe was not found."); throw new IOException("Unable to compress ffmpeg.exe was not found.");
} }
if(!exeFile.canExecute()) { if (!exeFile.canExecute()) {
throw new IOException("Unable to compress ffmpeg.exe could not be execute"); throw new IOException("Unable to compress ffmpeg.exe could not be execute");
} }
if(outputFile.exists()) { if (outputFile.exists()) {
throw new IOException(String.format("Failed to compress %s, output file already exists %s", inputFile.toString(), outputFile.toString())); throw new IOException(String.format("Failed to compress %s, output file already exists %s", inputFile.toString(), outputFile.toString()));
} }
ProcessBuilder processBuilder = buildProcessWithRunAsInvoker( ProcessBuilder processBuilder = buildProcessWithRunAsInvoker(
"\"" + exeFile.getAbsolutePath() + "\"", "\"" + exeFile.getAbsolutePath() + "\"",
"-i", "\"" + inputFile.toString() + "\"", "-i", "\"" + inputFile.toString() + "\"",
"-vcodec", "libx264", "-vcodec", "libx264",
"-crf", "28", "-crf", "28",
"\"" + outputFile.toString() + "\""); "\"" + outputFile.toString() + "\"");
ExecUtil.execute(processBuilder, terminator); processBuilder.redirectError(ffmpegStderr);
processBuilder.redirectOutput(ffmpegStdout);
return ExecUtil.execute(processBuilder, terminator);
} }
/**
* Create a new video with the given width and height. An exception will
* be thrown if the output file exists.
*
* @param inputFile The video AbstractFile.
* @param outputFile Path for scaled file.
* @param width New video width.
* @param height New video height.
* @param terminator A processTerminator for the ffmpeg executable.
* @throws IOException
*/
static public void scaleVideo(AbstractFile inputFile, File outputFile, int width, int height, ExecUtil.ProcessTerminator terminator) throws IOException {
scaleVideo(getVideoFile(inputFile), outputFile, width, height, terminator);
}
/**
* Create a new video with the given width and height. An exception will be
* thrown if the output file exists.
*
* @param inputPath Absolute path to input video.
* @param outputFile Path for scaled file.
* @param width New video width.
* @param height New video height.
* @param terminator A processTerminator for the ffmpeg executable.
* @throws IOException
*/
private static void scaleVideo(File inputFile, File outputFile, int width, int height, ExecUtil.ProcessTerminator terminator) throws IOException{
Path executablePath = Paths.get(FFMPEG, FFMPEG_EXE);
File exeFile = InstalledFileLocator.getDefault().locate(executablePath.toString(), VideoUtils.class.getPackage().getName(), true);
if(exeFile == null) {
throw new IOException("Unable to compress ffmpeg.exe was not found.");
}
if(!exeFile.canExecute()) {
throw new IOException("Unable to compress ffmpeg.exe could not be execute");
}
if(outputFile.exists()) {
throw new IOException(String.format("Failed to compress %s, output file already exists %s", inputFile.toString(), outputFile.toString()));
}
String scaleParam = Integer.toString(width) + ":" + Integer.toString(height);
ProcessBuilder processBuilder = buildProcessWithRunAsInvoker(
"\"" + exeFile.getAbsolutePath() + "\"",
"-i", "\"" + inputFile.toString() + "\"",
"-s", scaleParam,
"-c:a", "copy",
"\"" + outputFile.toString() + "\"");
ExecUtil.execute(processBuilder, terminator);
}
/** /**
* Returns a File object representing a temporary copy of the video file * Returns a File object representing a temporary copy of the video file
* representing by the AbstractFile object. * representing by the AbstractFile object.
* *
* @param file * @param file
* @return * @return
*/ */
static File getVideoFile(AbstractFile file) { static File getVideoFile(AbstractFile file) {
java.io.File tempFile; java.io.File tempFile;
try { try {
tempFile = getVideoFileInTempDir(file); tempFile = getVideoFileInTempDir(file);
} catch (NoCurrentCaseException ex) { } catch (NoCurrentCaseException ex) {
@ -378,10 +355,10 @@ public class VideoUtils {
progress.finish(); progress.finish();
} }
} }
return tempFile; return tempFile;
} }
static private ProcessBuilder buildProcessWithRunAsInvoker(String... commandLine) { static private ProcessBuilder buildProcessWithRunAsInvoker(String... commandLine) {
ProcessBuilder processBuilder = new ProcessBuilder(commandLine); ProcessBuilder processBuilder = new ProcessBuilder(commandLine);
/* /*
@ -391,6 +368,111 @@ public class VideoUtils {
processBuilder.environment().put("__COMPAT_LAYER", "RunAsInvoker"); //NON-NLS processBuilder.environment().put("__COMPAT_LAYER", "RunAsInvoker"); //NON-NLS
return processBuilder; return processBuilder;
} }
// Defaults for the screen capture methods.
private static final long MIN_FRAME_INTERVAL_MILLIS = 500;
private static final int DEFAULT_FRAMES_PER_SECOND = 1;
private static final int DEFAULT_TOTAL_FRAMES = 100;
private static final int DEFAULT_SCALE = 100;
/**
* Creates an output video containing a series of screen captures from the
* input video.
*
* @param inputFile
* @param outputFile
*
* @throws IOException
*/
static public void createScreenCaptureVideo(File inputFile, File outputFile) throws IOException{
createScreenCaptureVideo(inputFile, outputFile, DEFAULT_TOTAL_FRAMES, DEFAULT_SCALE, DEFAULT_FRAMES_PER_SECOND);
}
/**
* Creates a video containing a series of screen captures from the input
* video.
* @param inputFile File representing the input video.
* @param outputFile File representing the output video.
* An exception will be thrown if the output video exists.
* @param numFrames Total number of screen captures to included in the video.
* @param scale Percentage to scale the screen captures. The value of
* 0 or 100 will cause no change.
* @param framesPerSecond The number of frames to show in the video each second
* The lower this value the longer the image will appear on the screen.
* @throws IOException
*/
static public void createScreenCaptureVideo(File inputFile, File outputFile, long numFrames, double scale, int framesPerSecond) throws IOException{
if(outputFile.exists()) {
throw new IOException(String.format("Failed to compress %s, output file already exists %s", inputFile.getName(), outputFile.toString()));
}
if (!inputFile.exists() || !inputFile.canRead()) {
throw new IOException(String.format("Failed to compress %s, input file cannot be read.", inputFile.getName()));
}
outputFile.mkdirs();
String file_name = inputFile.toString();//OpenCV API requires string for file name
VideoCapture videoCapture = new VideoCapture(file_name); //VV will contain the videos
if (!videoCapture.isOpened()) //checks if file is not open
{
// add this file to the set of known bad ones
throw new IOException("Problem with video file; problem when attempting to open file.");
}
VideoWriter writer = null;
try {
// get the duration of the video
double fps = framesPerSecond == 0 ? videoCapture.get(Videoio.CAP_PROP_FPS) : framesPerSecond; // gets frame per second
double total_frames = videoCapture.get(7); // gets total frames
double milliseconds = 1000 * (total_frames / fps); //convert to ms duration
long myDurationMillis = (long) milliseconds;
if (myDurationMillis <= 0) {
throw new IOException(String.format("Failed to make snapshot video, original video has no duration. %s", inputFile.toString()));
}
// calculate the number of frames to capture
int numFramesToGet = (int) numFrames;
long frameInterval = myDurationMillis / numFrames;
if (frameInterval < MIN_FRAME_INTERVAL_MILLIS) {
numFramesToGet = 1;
}
// for each timeStamp, grab a frame
Mat mat = new Mat(); // holds image received from video in mat format (opencv format)
Size s = new Size((int) videoCapture.get(Videoio.CAP_PROP_FRAME_WIDTH), (int) videoCapture.get(Videoio.CAP_PROP_FRAME_HEIGHT));
Size newSize = (scale == 0 || scale == 100) ? s : new Size((int) (videoCapture.get(Videoio.CAP_PROP_FRAME_WIDTH) * scale), (int) (videoCapture.get(Videoio.CAP_PROP_FRAME_HEIGHT) * scale));
writer = new VideoWriter(outputFile.toString(), VideoWriter.fourcc('M', 'J', 'P', 'G'), 1, newSize, true);
if (!writer.isOpened()) {
throw new IOException(String.format("Problem with video file; problem when attempting to open output file. %s", outputFile.toString()));
}
for (int frame = 0; frame < numFramesToGet; ++frame) {
long timeStamp = frame * frameInterval;
videoCapture.set(0, timeStamp); //set video in timeStamp ms
if (!videoCapture.read(mat)) { // on Wav files, usually the last frame we try to read does not succeed.
continue;
}
Mat resized = new Mat();
Imgproc.resize(mat, resized, newSize, 0, 0, Imgproc.INTER_CUBIC);
writer.write(resized);
}
} finally {
videoCapture.release();
if (writer != null) {
writer.release();
}
}
}
/** /**
* Gets a File object in the temp directory of the current case for the * Gets a File object in the temp directory of the current case for the
@ -411,5 +493,5 @@ public class VideoUtils {
throw new IllegalStateException(ex); throw new IllegalStateException(ex);
} }
} }
} }