Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.jhotdraw8.draw.io.SimpleXmlReader Maven / Gradle / Ivy
/*
* @(#)SimpleXmlStaxReader.java
* Copyright © 2023 The authors and contributors of JHotDraw. MIT License.
*/
package org.jhotdraw8.draw.io;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import org.jhotdraw8.base.converter.IdFactory;
import org.jhotdraw8.draw.figure.Drawing;
import org.jhotdraw8.draw.figure.Figure;
import org.jhotdraw8.draw.figure.GroupFigure;
import org.jhotdraw8.draw.figure.Layer;
import org.jhotdraw8.draw.figure.StyleableFigure;
import org.jhotdraw8.draw.input.ClipboardInputFormat;
import org.jhotdraw8.draw.model.DrawingModel;
import org.jhotdraw8.fxbase.concurrent.SimpleWorkState;
import org.jhotdraw8.fxbase.concurrent.WorkState;
import org.jhotdraw8.fxcollection.typesafekey.MapAccessor;
import org.jhotdraw8.icollection.VectorList;
import org.jhotdraw8.icollection.immutable.ImmutableList;
import org.jhotdraw8.xml.XmlUtil;
import org.jspecify.annotations.Nullable;
import javax.xml.stream.Location;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stream.StreamSource;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.UncheckedIOException;
import java.net.URI;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.SequencedSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.FutureTask;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This reader does not support {@link FigureFactory#nodeListToValue(MapAccessor, List)}.
*/
public class SimpleXmlReader extends AbstractInputFormat implements ClipboardInputFormat {
private static final Pattern hrefPattern = Pattern.compile("\\s+href=\"([^\"]*?)\"");
private final IdFactory idFactory;
private @Nullable String namespaceURI;
private FigureFactory figureFactory;
private final String idAttribute = "id";
private Supplier layerFactory;
public SimpleXmlReader(FigureFactory figureFactory, IdFactory idFactory, @Nullable String namespaceURI) {
this.idFactory = idFactory;
this.figureFactory = figureFactory;
this.namespaceURI = namespaceURI;
}
private Figure createFigure(XMLStreamReader r, Deque stack) throws IOException {
Figure figure;
try {
figure = figureFactory.createFigureByElementName(r.getLocalName());
} catch (IOException e) {
throw new IOException("Cannot create figure for element <" + r.getLocalName() + "> at line " + r.getLocation().getLineNumber() + ", col " + r.getLocation().getColumnNumber(), e);
}
if (stack.isEmpty()) {
stack.addFirst(figure);// add twice, so that it will remain after we finish the file
} else {
Figure parent = stack.peek();
if (!figure.isSuitableParent(parent) || !parent.isSuitableChild(figure)) {
throw new IOException("Cannot add figure to parent in element <" + r.getLocalName() + "> at line " + r.getLocation().getLineNumber() + ", col " + r.getLocation().getColumnNumber());
}
parent.getChildren().add(figure);
}
stack.addFirst(figure);
return figure;
}
private DataFormat getDataFormat() {
String mimeType = "application/xml";
DataFormat df = DataFormat.lookupMimeType(mimeType);
if (df == null) {
df = new DataFormat(mimeType);
}
return df;
}
public IdFactory getIdFactory() {
return idFactory;
}
public Supplier getLayerFactory() {
return layerFactory;
}
public void setLayerFactory(Supplier layerFactory) {
this.layerFactory = layerFactory;
}
@Override
public Figure read(InputStream in, @Nullable Drawing drawing, @Nullable URI documentHome, WorkState workState) throws IOException {
return read((AutoCloseable) in, drawing, documentHome, workState);
}
/**
* Reads from the specified input stream.
*
* @param in must be an instanceof {@link InputStream} or of {@link Reader}.
* @return a deque of the figures in the files
* @throws IOException on IO failure
*/
private Deque doRead(AutoCloseable in) throws IOException {
Deque stack = new ArrayDeque<>();
List secondPass = new ArrayList<>();
List parallelPass = new ArrayList<>();
List> futures = new ArrayList<>();
List> processingInstructions = new ArrayList<>();
try {
XMLStreamReader xmlStreamReader = XmlUtil.streamReader(
(in instanceof InputStream inputStream) ? new StreamSource(inputStream)
: new StreamSource((Reader)in));
while (xmlStreamReader.hasNext()) {
readNode(xmlStreamReader, xmlStreamReader.next(), stack, processingInstructions,
secondPass, parallelPass, futures);
}
} catch (XMLStreamException e) {
throw new IOException(e);
}
if (stack.size() != 1) {
throw new IOException("The file does not contain a root element in namespace=\"" + namespaceURI + "\".");
}
for (Consumer processingInstruction : processingInstructions) {
processingInstruction.accept(stack.getFirst());
}
forkParallelPass(futures, parallelPass);
try {
secondPass.parallelStream().forEach(Runnable::run);
} catch (UncheckedIOException e) {
throw e.getCause();
}
try {
// If there is not enough parallelism, this future may never return!
for (FutureTask future : futures) {
future.get();
}
} catch (InterruptedException | ExecutionException e) {
throw new IOException(e);
}
return stack;
}
public @Nullable Figure read(Reader in, @Nullable Drawing drawing, @Nullable URI documentHome, WorkState workState) throws IOException {
return read((AutoCloseable) in, drawing, documentHome, workState);
}
private Figure read(AutoCloseable in, @Nullable Drawing drawing, @Nullable URI documentHome, WorkState workState) throws IOException {
workState.updateProgress(0.0);
idFactory.setDocumentHome(documentHome);
Deque stack = doRead(in);
Figure figure = stack.isEmpty() ? null : stack.getFirst();
if (figure == null) {
throw new IOException("Input file is empty.");
}
if ((figure instanceof Drawing)) {
figure.set(Drawing.DOCUMENT_HOME, documentHome);
}
workState.updateProgress(1.0);
return figure;
}
@Override
public SequencedSet read(Clipboard clipboard, DrawingModel model, Drawing drawing, @Nullable Figure parent) throws IOException {
Object content = clipboard.getContent(getDataFormat());
if (content instanceof String) {
SequencedSet figures = new LinkedHashSet<>();
Figure newDrawing = read(new StringReader((String) content), null, null, new SimpleWorkState<>());
if (newDrawing == null) {
return figures;
}
idFactory.reset();
idFactory.setDocumentHome(null);
for (Figure f : drawing.preorderIterable()) {
idFactory.createId(f);
}
if (parent == null) {
parent = layerFactory.get();
}
if (parent.getDrawing() != drawing) {
model.addChildTo(parent, drawing);
}
for (Figure f : new ArrayList<>(newDrawing.getChildren())) {
figures.add(f);
newDrawing.removeChild(f);
String id = idFactory.createId(f);
f.set(StyleableFigure.ID, id);
if (f instanceof Layer) {
model.addChildTo(f, drawing);
} else {
model.addChildTo(f, parent);
}
}
return figures;
} else {
throw new IOException("no data found");
}
}
private void readAttributes(XMLStreamReader r, Figure figure, List secondPass, List parallelPass) throws IOException {
for (int i = 0, n = r.getAttributeCount(); i < n; i++) {
String ns = r.getAttributeNamespace(i);
if (namespaceURI != null && ns != null && !namespaceURI.equals(ns)) {
continue;
}
String attributeLocalName = r.getAttributeLocalName(i);
String attributeValue = r.getAttributeValue(i);
Location location = r.getLocation();
if (idAttribute.equals(attributeLocalName)) {
idFactory.putIdToObject(attributeValue, figure);
setId(figure, attributeValue);
} else {
@SuppressWarnings("unchecked")
MapAccessor key =
(MapAccessor) figureFactory.getKeyByAttributeName(figure, attributeLocalName);
if (key == null) {
throw new IOException("Unsupported attribute \"" + attributeLocalName + "\" at line " + location.getLineNumber() + ", col " + location.getColumnNumber());
}
List pass = figureFactory.needsIdResolver(key) ? secondPass : parallelPass;
pass.add(() -> {
try {
figure.set(key, figureFactory.stringToValue(key, attributeValue));
} catch (IOException e) {
throw new UncheckedIOException(
"Error reading attribute" + attributeLocalName + "\" at line " + location.getLineNumber() + ", col " + location.getColumnNumber(),
e);
}
});
}
}
}
private void readEndElement(XMLStreamReader r, Deque stack) {
stack.removeFirst();
}
private void readNode(XMLStreamReader r, int next, Deque stack,
List> processingInstructions,
List secondPass, List parallelPass,
List> futures) throws IOException {
switch (next) {
case XMLStreamReader.START_ELEMENT:
readStartElement(r, stack, secondPass, parallelPass);
break;
case XMLStreamReader.END_ELEMENT:
readEndElement(r, stack);
break;
case XMLStreamReader.PROCESSING_INSTRUCTION:
Consumer processingInstruction = readProcessingInstruction(r, stack, secondPass);
if (processingInstruction != null) {
processingInstructions.add(processingInstruction);
}
break;
case XMLStreamReader.CHARACTERS:
case XMLStreamReader.ENTITY_DECLARATION:
case XMLStreamReader.NOTATION_DECLARATION:
case XMLStreamReader.NAMESPACE:
case XMLStreamReader.CDATA:
case XMLStreamReader.DTD:
case XMLStreamReader.ATTRIBUTE:
case XMLStreamReader.ENTITY_REFERENCE:
case XMLStreamReader.END_DOCUMENT:
case XMLStreamReader.START_DOCUMENT:
case XMLStreamReader.SPACE:
case XMLStreamReader.COMMENT:
break;
default:
throw new IOException("unsupported XMLStream event: " + next);
}
if (parallelPass.size() > 1_000) {
ArrayList runParallel = new ArrayList<>(parallelPass);
parallelPass.clear();
forkParallelPass(futures, runParallel);
}
}
private void forkParallelPass(List> futures, List runParallel) {
FutureTask task = new FutureTask<>(() -> {
for (final Runnable runner : runParallel) {
runner.run();
}
return null;
});
if (ForkJoinPool.getCommonPoolParallelism() < 2) {
// When there is not enough parallelism, then the reader may saturate
// the pool!
task.run();
} else {
ForkJoinPool.commonPool().execute(task);
}
futures.add(task);
}
private @Nullable Consumer readProcessingInstruction(XMLStreamReader r, Deque stack, List secondPass) {
if (figureFactory.getStylesheetsKey() != null) {
String piTarget = r.getPITarget();
String piData = r.getPIData();
if ("xml-stylesheet".equals(piTarget) && piData != null) {
return (drawing -> {
Matcher m = hrefPattern.matcher(piData);
if (m.find()) {
String href = m.group(1);
URI uri = URI.create(href);
uri = idFactory.absolutize(uri);
ImmutableList listOrNull = drawing.get(figureFactory.getStylesheetsKey());
List stylesheets = listOrNull == null ? new ArrayList<>() : new ArrayList<>(listOrNull.asList());
stylesheets.add(uri);
drawing.set(figureFactory.getStylesheetsKey(), VectorList.copyOf(stylesheets));
}
});
}
}
return null;
}
private void readStartElement(XMLStreamReader r, Deque stack,
List secondPass, List parallelPass) throws IOException {
if (namespaceURI != null && !namespaceURI.equals(r.getNamespaceURI())) {
stack.push(new GroupFigure());// push a dummy figure
return;
}
Figure figure = createFigure(r, stack);
readAttributes(r, figure, secondPass, parallelPass);
}
public void setFigureFactory(FigureFactory figureFactory) {
this.figureFactory = figureFactory;
}
protected void setId(Figure figure, String id) {
figure.set(StyleableFigure.ID, id);
}
public void setNamespaceURI(@Nullable String namespaceURI) {
this.namespaceURI = namespaceURI;
}
}