/*
* ShootOFF - Software for Laser Dry Fire Training
* Copyright (C) 2016 phrack
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.shootoff.targets.io;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import com.shootoff.gui.controller.TargetEditorController;
import com.shootoff.targets.EllipseRegion;
import com.shootoff.targets.ImageRegion;
import com.shootoff.targets.PolygonRegion;
import com.shootoff.targets.RectangleRegion;
import com.shootoff.targets.TargetRegion;
import com.shootoff.targets.animation.GifAnimation;
import com.shootoff.targets.animation.SpriteAnimation;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Shape;
public class XMLTargetReader implements TargetReader {
private static final Logger logger = LoggerFactory.getLogger(XMLTargetReader.class);
private final List<Node> targetNodes = new ArrayList<>();
private final Map<String, String> targetTags = new HashMap<>();
private final boolean playAnimations;
private final Optional<ClassLoader> loader;
public XMLTargetReader(File targetFile, boolean playAnimations) {
this.playAnimations = playAnimations;
loader = Optional.empty();
try (InputStream is = new FileInputStream(targetFile)) {
load(is);
} catch (final IOException e) {
logger.error("Problem initializing target reader from file", e);
}
}
public XMLTargetReader(InputStream targetStream, boolean playAnimations) {
this.playAnimations = playAnimations;
loader = Optional.empty();
load(targetStream);
}
public XMLTargetReader(InputStream targetStream, boolean playAnimations, ClassLoader loader) {
this.playAnimations = playAnimations;
this.loader = Optional.ofNullable(loader);
load(targetStream);
}
@Override
public List<Node> getTargetNodes() {
return targetNodes;
}
@Override
public Map<String, String> getTargetTags() {
return targetTags;
}
private void load(InputStream targetStream) {
try {
final SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
final TargetXMLHandler handler = new TargetXMLHandler();
saxParser.parse(targetStream, handler);
targetNodes.addAll(handler.getRegions());
targetTags.putAll(handler.getTags());
return;
} catch (IOException | ParserConfigurationException | SAXException e) {
logger.error("Error reading XML target", e);
} finally {
if (targetStream != null) {
try {
targetStream.close();
} catch (final IOException e) {
logger.error("Error closing XMl target opened for reading", e);
}
}
}
}
private class TargetXMLHandler extends DefaultHandler {
private final Map<String, String> targetTags = new HashMap<>();
private final List<Node> regions = new ArrayList<>();
private TargetRegion currentRegion;
private List<Double> polygonPoints = null;
private Color polygonFill = null;
private Map<String, String> currentTags;
public List<Node> getRegions() {
return regions;
}
public Map<String, String> getTags() {
return targetTags;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
switch (qName) {
case "target":
if (attributes.getLength() > 0) {
for (int i = 0; i < attributes.getLength(); i++) {
final String key = attributes.getQName(i);
final String value = attributes.getValue(key);
targetTags.put(key, value);
}
}
break;
case "image":
currentTags = new HashMap<>();
final File savedFile = new File(attributes.getValue("file"));
InputStream imageStream = null;
if ('@' == savedFile.toString().charAt(0) && loader.isPresent()) {
imageStream = loader.get().getResourceAsStream(savedFile.toString().substring(1).replace("\\", "/"));
}
File imageFile;
if (savedFile.isAbsolute() || '@' == savedFile.toString().charAt(0)) {
imageFile = savedFile;
} else {
imageFile = new File(
System.getProperty("shootoff.home") + File.separator + attributes.getValue("file"));
}
ImageRegion imageRegion;
if (imageStream != null) {
imageRegion = new ImageRegion(Double.parseDouble(attributes.getValue("x")),
Double.parseDouble(attributes.getValue("y")), imageFile, imageStream);
} else {
try {
imageRegion = new ImageRegion(Double.parseDouble(attributes.getValue("x")),
Double.parseDouble(attributes.getValue("y")), imageFile);
} catch (final FileNotFoundException e) {
logger.error("Failed to load target image from file: {}", e);
return;
}
}
try {
final int firstDot = imageFile.getName().indexOf('.') + 1;
final String extension = imageFile.getName().substring(firstDot);
if (extension.endsWith("gif") && '@' == savedFile.toString().charAt(0) && loader.isPresent()) {
final InputStream gifStream = loader.get().getResourceAsStream(savedFile.toString().substring(1).replace("\\", "/"));
final GifAnimation gif = new GifAnimation(imageRegion, gifStream);
imageRegion.setImage(gif.getFirstFrame());
if (gif.getFrameCount() > 1) imageRegion.setAnimation(gif);
} else if (extension.endsWith("gif")) {
final GifAnimation gif = new GifAnimation(imageRegion, imageRegion.getImageFile());
imageRegion.setImage(gif.getFirstFrame());
if (gif.getFrameCount() > 1) imageRegion.setAnimation(gif);
}
if (imageRegion.getAnimation().isPresent() && playAnimations) {
final SpriteAnimation animation = imageRegion.getAnimation().get();
animation.setCycleCount(1);
animation.setOnFinished((e) -> {
animation.reset();
animation.setOnFinished(null);
});
animation.play();
}
} catch (final IOException e) {
logger.error("Error reading animation from XML target", e);
}
currentRegion = imageRegion;
break;
case "rectangle":
currentTags = new HashMap<>();
currentRegion = new RectangleRegion(Double.parseDouble(attributes.getValue("x")),
Double.parseDouble(attributes.getValue("y")), Double.parseDouble(attributes.getValue("width")),
Double.parseDouble(attributes.getValue("height")));
((Shape) currentRegion).setFill(TargetEditorController.createColor(attributes.getValue("fill")));
break;
case "ellipse":
currentTags = new HashMap<>();
currentRegion = new EllipseRegion(Double.parseDouble(attributes.getValue("centerX")),
Double.parseDouble(attributes.getValue("centerY")),
Double.parseDouble(attributes.getValue("radiusX")),
Double.parseDouble(attributes.getValue("radiusY")));
((Shape) currentRegion).setFill(TargetEditorController.createColor(attributes.getValue("fill")));
break;
case "polygon":
currentTags = new HashMap<>();
polygonPoints = new ArrayList<>();
polygonFill = TargetEditorController.createColor(attributes.getValue("fill"));
break;
case "point":
polygonPoints.add(Double.parseDouble(attributes.getValue("x")));
polygonPoints.add(Double.parseDouble(attributes.getValue("y")));
break;
case "tag":
currentTags.put(attributes.getValue("name"), attributes.getValue("value"));
break;
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
switch (qName) {
case "polygon":
final double[] points = new double[polygonPoints.size()];
for (int i = 0; i < polygonPoints.size(); i++)
points[i] = polygonPoints.get(i);
currentRegion = new PolygonRegion(points);
((Shape) currentRegion).setFill(polygonFill);
case "image":
case "rectangle":
case "ellipse":
currentRegion.setTags(currentTags);
regions.add((Node) currentRegion);
break;
}
}
}
}