org.dita.dost.reader.ConrefPushReader Maven / Gradle / Ivy
The newest version!
/*
* This file is part of the DITA Open Toolkit project.
*
* Copyright 2010 IBM Corporation
*
* See the accompanying LICENSE file for applicable license.
*/
package org.dita.dost.reader;
import static org.dita.dost.util.Constants.*;
import static org.dita.dost.util.URLUtils.toFile;
import static org.dita.dost.util.URLUtils.toURI;
import java.io.File;
import java.net.URI;
import java.util.*;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.dom.DOMResult;
import org.dita.dost.log.MessageUtils;
import org.dita.dost.util.FileUtils;
import org.dita.dost.util.XMLUtils;
import org.w3c.dom.*;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
/**
* Class for reading conref push content.
*
*/
public final class ConrefPushReader extends AbstractXMLReader {
/** push table.*/
private final Hashtable> pushtable;
/** Document used to construct push table DocumentFragments. */
private final Document pushDocument;
/**keep the file path of current file under parse
filePath is useful to get the absolute path of the target file.*/
private File fileDir = null;
/**keep the file name of current file under parse */
private File parsefilename = null;
/**pushcontent is used to store the content copied to target
in pushcontent href will be resolved if it is relative path
if @conref is in pushconref the target name should be recorded so that it
could be added to conreflist for conref resolution.*/
private XMLStreamWriter pushcontentWriter;
/** Common document for all push content document fragments. */
private DocumentFragment pushcontentDocumentFragment;
/**boolean start is used to control whether sax parser can start to
record push content into String pushcontent.*/
private boolean start = false;
/**level is used to record the level number to the root element in pushcontent
In endElement(...) we can turn start off to terminate adding content to pushcontent
if level is zero. That means we reach the end tag of the starting element.*/
private int level = 0;
/**target is used to record the target of the conref push
if we reach pushafter action but there is no target recorded before, we need
to report error.*/
private URI target = null;
/**pushType is used to record the current type of push
it is used in endElement(....) to tell whether it is pushafter or replace.*/
private String pushType = null;
/**
* Get push table
*
* @return unmodifiable push table
*/
public Map> getPushMap() {
return Collections.unmodifiableMap(pushtable);
}
@Override
public void read(final File filename) {
assert filename.isAbsolute();
fileDir = filename.getParentFile().getAbsoluteFile();
parsefilename = new File(filename.getName());
start = false;
pushcontentWriter = getXMLStreamWriter();
pushType = null;
try {
job.getStore().transform(filename.toURI(), this);
} catch (final RuntimeException e) {
throw e;
} catch (final Exception e) {
logger.error(e.getMessage(), e);
}
}
private XMLStreamWriter getXMLStreamWriter() {
pushcontentDocumentFragment = pushDocument.createDocumentFragment();
try {
return XMLOutputFactory.newInstance().createXMLStreamWriter(new DOMResult(pushcontentDocumentFragment));
} catch (final XMLStreamException | FactoryConfigurationError e) {
throw new RuntimeException(e);
}
}
/**
* Constructor.
*/
public ConrefPushReader() {
pushtable = new Hashtable<>();
pushDocument = XMLUtils.getDocumentBuilder().newDocument();
}
@Override
public void startElement(final String uri, final String localName, final String name, final Attributes atts)
throws SAXException {
if (start) {
//if start is true, we need to record content in pushcontent
//also we need to add level to make sure start is turn off
//at the corresponding end element
level++;
putElement(name, atts, false);
}
final String conactValue = atts.getValue(ATTRIBUTE_NAME_CONACTION);
if (!start && conactValue != null) {
switch (conactValue) {
case ATTR_CONACTION_VALUE_PUSHBEFORE -> {
if (pushcontentDocumentFragment.getChildNodes().getLength() != 0) {
// there are redundant "pushbefore", create a new pushcontent and emit a warning message.
if (pushcontentWriter != null) {
try {
pushcontentWriter.close();
} catch (final XMLStreamException e) {
throw new SAXException(e);
}
}
pushcontentWriter = getXMLStreamWriter();
logger.warn(MessageUtils.getMessage("DOTJ044W").setLocation(atts).toString());
}
start = true;
level = 1;
putElement(name, atts, true);
pushType = ATTR_CONACTION_VALUE_PUSHBEFORE;
}
case ATTR_CONACTION_VALUE_PUSHAFTER -> {
start = true;
level = 1;
if (target == null) {
logger.error(MessageUtils.getMessage("DOTJ039E").setLocation(atts).toString());
} else {
putElement(name, atts, true);
pushType = ATTR_CONACTION_VALUE_PUSHAFTER;
}
}
case ATTR_CONACTION_VALUE_PUSHREPLACE -> {
start = true;
level = 1;
target = toURI(atts.getValue(ATTRIBUTE_NAME_CONREF));
if (target == null) {
logger.error(MessageUtils.getMessage("DOTJ040E").setLocation(atts).toString());
} else {
pushType = ATTR_CONACTION_VALUE_PUSHREPLACE;
putElement(name, atts, true);
}
}
case ATTR_CONACTION_VALUE_MARK -> {
target = toURI(atts.getValue(ATTRIBUTE_NAME_CONREF));
if (target == null) {
logger.error(MessageUtils.getMessage("DOTJ068E").setLocation(atts).toString());
}
if (
target != null &&
pushcontentDocumentFragment != null &&
pushcontentDocumentFragment.getChildNodes().getLength() > 0 &&
ATTR_CONACTION_VALUE_PUSHBEFORE.equals(pushType)
) {
//pushcontent != null means it is pushbefore action
//we need to add target and content to pushtable
if (pushcontentWriter != null) {
try {
pushcontentWriter.close();
} catch (final XMLStreamException e) {
throw new SAXException(e);
}
}
addtoPushTable(target, replaceContent(pushcontentDocumentFragment), pushType);
pushcontentWriter = getXMLStreamWriter();
target = null;
pushType = null;
}
}
}
}
}
/**
* Rewrite link attributes.
*/
private DocumentFragment replaceContent(final DocumentFragment pushcontent) {
final NodeList children = pushcontent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
final Node child = children.item(i);
switch (child.getNodeType()) {
case Node.ELEMENT_NODE -> {
final Element e = (Element) child;
replaceLinkAttributes(e);
final NodeList elements = e.getElementsByTagName("*");
for (int j = 0; i < elements.getLength(); i++) {
replaceLinkAttributes((Element) elements.item(j));
}
}
}
}
return pushcontent;
}
private void replaceLinkAttributes(final Element pushcontent) {
for (final String attName : new String[] { ATTRIBUTE_NAME_HREF, ATTRIBUTE_NAME_CONREF }) {
final Attr att = pushcontent.getAttributeNode(attName);
if (att != null) {
att.setNodeValue(replaceURL(att.getNodeValue()));
}
}
}
/**
* Write start element.
*
* @param elemName element name
* @param atts attribute
* @param removeConref whether remeove conref info
* @throws SAXException if writing element failed
*/
private void putElement(final String elemName, final Attributes atts, final boolean removeConref)
throws SAXException {
//parameter boolean removeConref specifies whether to remove
//conref information like @conref @conaction in current element
//when copying it to pushcontent. True means remove and false means
//not remove.
final Set namespaces = new HashSet<>();
try {
pushcontentWriter.writeStartElement(elemName);
for (int index = 0; index < atts.getLength(); index++) {
final String name = atts.getQName(index);
if (!removeConref || !ATTRIBUTE_NAME_CONREF.equals(name) && !ATTRIBUTE_NAME_CONACTION.equals(name)) {
String value = atts.getValue(index);
if (ATTRIBUTE_NAME_HREF.equals(name) || ATTRIBUTE_NAME_CONREF.equals(name)) {
// adjust href for pushbefore and replace
value = replaceURL(value);
}
final int offset = atts.getQName(index).indexOf(":");
final String prefix = offset != -1 ? atts.getQName(index).substring(0, offset) : "";
if (!namespaces.contains(prefix)) {
namespaces.add(prefix);
if (!prefix.isEmpty()) {
pushcontentWriter.writeNamespace(prefix, atts.getURI(index));
}
}
pushcontentWriter.writeAttribute(prefix, atts.getURI(index), atts.getLocalName(index), value);
}
}
//id attribute should only be added to the starting element
//which dosen't have id attribute set
if (ATTR_CONACTION_VALUE_PUSHREPLACE.equals(pushType) && atts.getValue(ATTRIBUTE_NAME_ID) == null && level == 1) {
final String fragment = target.getFragment();
if (fragment == null) {
//if there is no '#' in target string, report error
logger.error(MessageUtils.getMessage("DOTJ041E", target.toString()).setLocation(atts).toString());
} else {
String id;
//has element id
if (fragment.contains(SLASH)) {
id = fragment.substring(fragment.lastIndexOf(SLASH) + 1);
} else {
id = fragment;
}
//add id attribute
pushcontentWriter.writeAttribute(ATTRIBUTE_NAME_ID, id);
}
}
} catch (final XMLStreamException e) {
throw new SAXException(e);
}
}
/**
*
* @param value string
* @return URL
*/
private String replaceURL(final String value) {
if (value == null) {
return null;
} else if (
target == null || FileUtils.isAbsolutePath(value) || value.contains(COLON_DOUBLE_SLASH) || value.startsWith(SHARP)
) {
return value;
} else {
final String source = FileUtils.resolve(fileDir, target).getPath();
final String urltarget = FileUtils.resolveTopic(fileDir, value);
return FileUtils.getRelativeUnixPath(source, urltarget);
}
}
/**
*
* @param target target
* @param pushcontent content
* @param type push type
*/
private void addtoPushTable(URI target, final DocumentFragment pushcontent, final String type) {
if (target.getFragment() == null) {
//if there is no '#' in target string, report error
logger.error(MessageUtils.getMessage("DOTJ041E", target.toString()).toString());
return;
}
if (target.getPath().isEmpty()) {
//means conref the file itself
target = toURI(parsefilename.getPath() + target);
}
final File key = toFile(FileUtils.resolve(fileDir, target));
Hashtable table;
if (pushtable.containsKey(key)) {
//if there is something else push to the same file
table = pushtable.get(key);
} else {
//if there is nothing else push to the same file
table = new Hashtable<>();
pushtable.put(key, table);
}
final MoveKey moveKey = new MoveKey(SHARP + target.getFragment(), type);
if (table.containsKey(moveKey)) {
//if there is something else push to the same target
//append content if type is 'pushbefore' or 'pushafter'
//report error if type is 'replace'
if (ATTR_CONACTION_VALUE_PUSHREPLACE.equals(type)) {
logger.error(MessageUtils.getMessage("DOTJ042E", target.toString()).toString());
} else {
table.put(moveKey, appendPushContent(pushcontent, table.get(moveKey)));
}
} else {
//if there is nothing else push to the same target
table.put(moveKey, appendPushContent(pushcontent, null));
}
}
private DocumentFragment appendPushContent(final DocumentFragment pushcontent, final DocumentFragment target) {
DocumentFragment df = target;
if (df == null) {
df = pushDocument.createDocumentFragment();
}
final NodeList children = pushcontent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
df.appendChild(pushDocument.importNode(children.item(i), true));
}
return df;
}
@Override
public void characters(final char[] ch, final int start, final int length) throws SAXException {
if (this.start) {
try {
pushcontentWriter.writeCharacters(ch, start, length);
} catch (XMLStreamException e) {
throw new SAXException(e);
}
}
}
@Override
public void endElement(final String uri, final String localName, final String name) throws SAXException {
if (start) {
level--;
try {
pushcontentWriter.writeEndElement();
} catch (XMLStreamException e) {
throw new SAXException(e);
}
}
if (level == 0) {
//turn off start if we reach the end tag of staring element
start = false;
if (ATTR_CONACTION_VALUE_PUSHAFTER.equals(pushType) || ATTR_CONACTION_VALUE_PUSHREPLACE.equals(pushType)) {
//if it is pushafter or replace, we need to record content in pushtable
//if target == null we have already reported error in startElement;
if (target != null) {
if (pushcontentWriter != null) {
try {
pushcontentWriter.close();
} catch (final XMLStreamException e) {
throw new SAXException(e);
}
}
addtoPushTable(target, pushcontentDocumentFragment, pushType);
pushcontentWriter = getXMLStreamWriter();
target = null;
pushType = null;
}
}
}
}
public record MoveKey(String idPath, String action) {
@Override
public String toString() {
return idPath + STICK + action;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy