/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* licenses this file to you 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 the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apereo.portal.xml.stream;
import java.io.StringWriter;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import org.apereo.portal.character.stream.CharacterEventSource;
import org.apereo.portal.character.stream.events.CharacterDataEventImpl;
import org.apereo.portal.character.stream.events.CharacterEvent;
/**
* Used with code that serializes StAX events into a string. Watches for specific XML tags in a StAX
* stream and chunks the string data at each XML tag occurrence.
*
*/
public class ChunkingEventReader extends BaseXMLEventReader {
private static final XMLEventFactory EVENT_FACTORY = XMLEventFactory.newFactory();
private final List<CharacterEvent> characterEvents = new LinkedList<CharacterEvent>();
private final HttpServletRequest request;
private final Map<String, CharacterEventSource> chunkingElements;
private final Map<Pattern, CharacterEventSource> chunkingPatternEventSources;
private final Pattern[] chunkingPatterns;
private final XMLEventWriter xmlEventWriter;
private final StringWriter writer;
private boolean removeXmlDeclaration = true;
//to handle peek() calls
private XMLEvent peekedEvent = null;
//Handle chunking immediately after a StartElement
private StartElement captureEvent = null;
public ChunkingEventReader(
HttpServletRequest request,
Map<String, CharacterEventSource> chunkingElements,
Map<Pattern, CharacterEventSource> chunkingPatternEventSources,
Pattern[] chunkingPatterns,
XMLEventReader xmlEventReader,
XMLEventWriter xmlEventWriter,
StringWriter writer) {
super(xmlEventReader);
this.request = request;
this.chunkingElements = chunkingElements;
this.chunkingPatternEventSources = chunkingPatternEventSources;
this.chunkingPatterns = chunkingPatterns;
this.xmlEventWriter = xmlEventWriter;
this.writer = writer;
}
public boolean isRemoveXmlDeclaration() {
return this.removeXmlDeclaration;
}
/** Remove the XML declaration from the output. Defaults to true */
public void setRemoveXmlDeclaration(boolean removeXmlDeclaration) {
this.removeXmlDeclaration = removeXmlDeclaration;
}
/** @return The character events generated by the reader */
public List<CharacterEvent> getCharacterEvents() {
return this.characterEvents;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public XMLEvent peek() throws XMLStreamException {
if (this.peekedEvent == null) {
this.peekedEvent = this.nextEvent();
}
return this.peekedEvent;
}
@Override
protected XMLEvent internalNextEvent() throws XMLStreamException {
XMLEvent event = null;
//Read from the buffer if there was a peek
if (this.peekedEvent != null) {
event = this.peekedEvent;
this.peekedEvent = null;
return event;
}
final XMLEvent previousEvent = this.getPreviousEvent();
if (previousEvent != null && previousEvent.isStartDocument()) {
this.xmlEventWriter.flush();
this.clearWriter();
}
if (this.captureEvent != null) {
event = this.tryChunking(this.captureEvent);
this.captureEvent = null;
} else {
event = this.getParent().nextEvent();
if (event.isStartElement()) {
final StartElement startElement = event.asStartElement();
event = this.tryChunking(startElement);
}
}
return event;
}
private XMLEvent tryChunking(StartElement startElement) throws XMLStreamException {
QName elementName = startElement.getName();
CharacterEventSource characterEventSource =
this.chunkingElements.get(elementName.getLocalPart());
while (characterEventSource != null) {
final XMLEvent previousEvent = this.getPreviousEvent();
if (previousEvent != null && previousEvent.isStartElement()) {
this.captureEvent = startElement;
//Write an empty Character event to force the serializer to finish writing the previous StartElement
//It is left open since ATTRIBUTE events can follow a START_ELEMENT event.
return EVENT_FACTORY.createCharacters("");
}
//Capture the characters written out to this point then clear the buffer
this.captureCharacterDataEvent();
//Get the generated events for the element
final XMLEventReader parent = this.getParent();
characterEventSource.generateCharacterEvents(
this.request, parent, startElement, this.characterEvents);
//Read the next event off the reader
final XMLEvent nextEvent = parent.nextEvent();
if (nextEvent.isStartElement()) {
startElement = nextEvent.asStartElement();
elementName = startElement.getName();
characterEventSource = this.chunkingElements.get(elementName.getLocalPart());
} else {
return nextEvent;
}
}
return startElement;
}
protected void captureCharacterDataEvent() throws XMLStreamException {
this.xmlEventWriter.flush();
//Add character chunk to events
this.chunkString(this.characterEvents, this.writer.toString(), 0);
this.clearWriter();
}
/** Delete all data in the Writer */
private void clearWriter() {
final StringBuffer buffer = this.writer.getBuffer();
buffer.delete(0, buffer.length());
}
/**
* Breaks up the String into a List of CharacterEvents based on the configured Map of Patterns
* to CharacterEventSources
*/
protected void chunkString(
final List<CharacterEvent> characterEvents,
final CharSequence buffer,
int patternIndex) {
//Iterate over the chunking patterns
for (; patternIndex < this.chunkingPatterns.length; patternIndex++) {
final Pattern pattern = this.chunkingPatterns[patternIndex];
final Matcher matcher = pattern.matcher(buffer);
if (matcher.find()) {
final CharacterEventSource eventSource =
this.chunkingPatternEventSources.get(pattern);
int prevMatchEnd = 0;
do {
//Add all of the text up to the match as a new chunk, use subSequence to avoid extra string alloc
this.chunkString(
characterEvents,
buffer.subSequence(prevMatchEnd, matcher.start()),
patternIndex + 1);
//Get the generated CharacterEvents for the match
final MatchResult matchResult = matcher.toMatchResult();
eventSource.generateCharacterEvents(this.request, matchResult, characterEvents);
prevMatchEnd = matcher.end();
} while (matcher.find());
//Add any remaining text from the original CharacterDataEvent
if (prevMatchEnd < buffer.length()) {
this.chunkString(
characterEvents,
buffer.subSequence(prevMatchEnd, buffer.length()),
patternIndex + 1);
}
return;
}
}
//Buffer didn't match anything, just append the string data
//de-duplication of the event string data
final String eventString = buffer.toString();
characterEvents.add(CharacterDataEventImpl.create(eventString));
}
@Override
public boolean hasNext() {
if (this.peekedEvent != null) {
return true;
}
if (!this.getParent().hasNext()) {
return false;
}
try {
this.peekedEvent = this.nextEvent();
} catch (NoSuchElementException e) {
return false;
} catch (XMLStreamException e) {
throw new RuntimeException(e);
}
return true;
}
@Override
public void close() throws XMLStreamException {
captureCharacterDataEvent();
this.getParent().close();
}
}