/*
* Copyright (c) 2007-2008 Mozilla Foundation
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package nu.validator.source;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.SortedSet;
import nu.validator.collections.HeadBiasedSortedSet;
import nu.validator.collections.TailBiasedSortedSet;
import nu.validator.htmlparser.common.CharacterHandler;
import nu.validator.xml.TypedInputSource;
import org.apache.log4j.Logger;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
public final class SourceCode implements CharacterHandler {
private static final Logger log4j = Logger.getLogger(SourceCode.class);
private static Location[] SOURCE_LOCATION_ARRAY_TYPE = new Location[0];
private String uri;
private String type;
private String encoding;
private int expectedLength;
private final SortedSet<Location> reverseSortedLocations = new HeadBiasedSortedSet<>(Collections.reverseOrder());
private final SortedSet<Location> exactErrors = new TailBiasedSortedSet<>();
private final SortedSet<Location> rangeLasts = new TailBiasedSortedSet<>();
private final SortedSet<Integer> oneBasedLineErrors = new TailBiasedSortedSet<>();
// private final SortedSet<Location> reverseSortedLocations = new TreeSet<>(Collections.reverseOrder());
//
// private final SortedSet<Location> exactErrors = new TreeSet<>();
//
// private final SortedSet<Location> rangeLasts = new TreeSet<>();
//
// private final SortedSet<Integer> oneBasedLineErrors = new TreeSet<>();
private final List<Line> lines = new ArrayList<>();
private Line currentLine = null;
private boolean prevWasCr = false;
private final LocationRecorder locationRecorder;
public SourceCode() {
this.locationRecorder = new LocationRecorder(this);
}
public void initialize(InputSource inputSource) {
this.uri = inputSource.getSystemId();
this.encoding = inputSource.getEncoding();
if (inputSource instanceof TypedInputSource) {
TypedInputSource typedInputSource = (TypedInputSource) inputSource;
int length = typedInputSource.getLength();
if (length == -1) {
expectedLength = 2048;
} else {
expectedLength = length;
}
this.type = typedInputSource.getType();
} else {
expectedLength = 2048;
this.type = null;
}
}
/**
* @see org.xml.sax.ContentHandler#characters(char[], int, int)
* @see java.lang.StringBuffer#append(char[], int, int)
*/
@Override
public void characters(char[] ch, int start, int length)
throws SAXException {
int s = start;
int end = start + length;
for (int i = start; i < end; i++) {
char c = ch[i];
switch (c) {
case '\r':
if (s < i) {
currentLine.characters(ch, s, i - s);
}
newLine();
s = i + 1;
prevWasCr = true;
break;
case '\n':
if (!prevWasCr) {
if (s < i) {
currentLine.characters(ch, s, i - s);
}
newLine();
}
s = i + 1;
prevWasCr = false;
break;
default:
prevWasCr = false;
break;
}
}
if (s < end) {
currentLine.characters(ch, s, end - s);
}
}
private void newLine() {
int offset;
char[] buffer;
if (currentLine == null) {
offset = 0;
buffer = new char[expectedLength];
} else {
offset = currentLine.getOffset() + currentLine.getBufferLength();
buffer = currentLine.getBuffer();
}
currentLine = new Line(buffer, offset);
lines.add(currentLine);
}
@Override
public void end() throws SAXException {
if (currentLine != null && currentLine.getBufferLength() == 0) {
// Theoretical impurity with line separators vs. terminators
lines.remove(lines.size() - 1);
currentLine = null;
}
}
@Override
public void start() throws SAXException {
reverseSortedLocations.clear();
lines.clear();
currentLine = null;
newLine();
prevWasCr = false;
}
void addLocatorLocation(int oneBasedLine, int oneBasedColumn) {
log4j.debug(oneBasedLine + ", " + oneBasedColumn);
reverseSortedLocations.add(new Location(this, oneBasedLine - 1,
oneBasedColumn - 1));
}
public void exactError(Location location, SourceHandler extractHandler)
throws SAXException {
exactErrors.add(location);
Location start = location.step(-15);
Location end = location.step(15);
extractHandler.startSource(type, encoding);
emitContent(start, location, extractHandler);
extractHandler.startCharHilite(location.getLine() + 1,
location.getColumn() + 1);
emitCharacter(location, extractHandler);
extractHandler.endCharHilite();
location = location.next();
emitContent(location, end, extractHandler);
extractHandler.endSource();
}
public void rememberExactError(Location location) {
if (location.getColumn() < 0 || location.getLine() < 0) {
return;
}
exactErrors.add(location);
}
public void registerRandeEnd(Locator locator) {
String systemId = locator.getSystemId();
if (uri == systemId || (uri != null && uri.equals(systemId))) {
rangeLasts.add(newLocatorLocation(locator.getLineNumber(), locator.getColumnNumber()));
}
}
public void rangeEndError(Location rangeStart, Location rangeLast,
SourceHandler extractHandler) throws SAXException {
reverseSortedLocations.add(rangeLast);
rangeLasts.add(rangeLast);
Location endRange = rangeLast.next();
Location start = rangeStart.step(-10);
Location end = endRange.step(6);
extractHandler.startSource(type, encoding);
emitContent(start, rangeStart, extractHandler);
extractHandler.startRange(rangeLast.getLine() + 1,
rangeLast.getColumn() + 1);
emitContent(rangeStart, endRange, extractHandler);
extractHandler.endRange();
emitContent(endRange, end, extractHandler);
extractHandler.endSource();
}
/**
* @param rangeLast
* @return
*/
public Location rangeStartForRangeLast(Location rangeLast) {
for (Location loc : reverseSortedLocations) {
if (loc.compareTo(rangeLast) < 0) {
return loc.next();
}
}
return new Location(this, 0, 0);
}
@SuppressWarnings("boxing")
public void lineError(int oneBasedLine, SourceHandler extractHandler)
throws SAXException {
oneBasedLineErrors.add(oneBasedLine);
Line line = lines.get(oneBasedLine - 1);
extractHandler.startSource(type, encoding);
extractHandler.characters(line.getBuffer(), line.getOffset(),
line.getBufferLength());
extractHandler.endSource();
}
public boolean isWithinKnownSource(Location location) {
if (location.getLine() >= lines.size()) {
return false;
}
Line line = lines.get(location.getLine());
return line.getBufferLength() >= location.getColumn();
}
public boolean isWithinKnownSource(int oneBasedLine) {
return !(oneBasedLine > lines.size());
}
Line getLine(int line) {
return lines.get(line);
}
int getNumberOfLines() {
return lines.size();
}
void emitCharacter(Location location, SourceHandler handler)
throws SAXException {
Line line = getLine(location.getLine());
int col = location.getColumn();
if (col == line.getBufferLength()) {
handler.newLine();
} else {
handler.characters(line.getBuffer(), line.getOffset() + col, 1);
}
}
/**
* Emits content between from a location (inclusive) until a location
* (exclusive).
*
* @param from
* @param until
* @param handler
* @throws SAXException
*/
void emitContent(Location from, Location until, SourceHandler handler)
throws SAXException {
if (from.compareTo(until) >= 0) {
return;
}
int fromLine = from.getLine();
int untilLine = until.getLine();
Line line = getLine(fromLine);
if (fromLine == untilLine) {
handler.characters(line.getBuffer(), line.getOffset()
+ from.getColumn(), until.getColumn() - from.getColumn());
} else {
// first line
int length = line.getBufferLength() - from.getColumn();
if (length > 0) {
handler.characters(line.getBuffer(), line.getOffset()
+ from.getColumn(), length);
}
if (fromLine + 1 != lines.size()) {
handler.newLine();
}
// lines in between
int wholeLine = fromLine + 1;
while (wholeLine < untilLine) {
line = getLine(wholeLine);
handler.characters(line.getBuffer(), line.getOffset(),
line.getBufferLength());
wholeLine++;
if (wholeLine != lines.size()) {
handler.newLine();
}
}
// last line
int untilCol = until.getColumn();
if (untilCol > 0) {
line = getLine(untilLine);
handler.characters(line.getBuffer(), line.getOffset(), untilCol);
}
}
}
public void emitSource(SourceHandler handler) throws SAXException {
List<Range> ranges = new LinkedList<>();
Location[] locations = reverseSortedLocations.toArray(SOURCE_LOCATION_ARRAY_TYPE);
int i = locations.length - 1;
for (Location loc : rangeLasts) {
while (i >= 0 && locations[i].compareTo(loc) < 0) {
i--;
}
Location start;
if (i == locations.length - 1) {
start = new Location(this, 0, 0);
} else {
start = locations[i + 1].next();
}
Location end = loc.next();
ranges.add(new Range(start, end, loc));
}
try {
handler.startSource(type, encoding);
handler.setLineErrors(oneBasedLineErrors);
Iterator<Range> rangeIter = ranges.iterator();
Iterator<Location> exactIter = exactErrors.iterator();
Location previousLocation = new Location(this, 0, 0);
Location exact = null;
Location rangeStart = null;
Location rangeEnd = null;
Location rangeLoc = null;
if (exactIter.hasNext()) {
exact = exactIter.next();
}
if (rangeIter.hasNext()) {
Range r = rangeIter.next();
rangeStart = r.getStart();
rangeEnd = r.getEnd();
rangeLoc = r.getLoc();
}
while (exact != null || rangeEnd != null) {
if (exact != null
&& (rangeStart == null || exact.compareTo(rangeStart) < 0)
&& (rangeEnd == null || exact.compareTo(rangeEnd) < 0)) { // exact
// first?
emitContent(previousLocation, exact, handler);
handler.startCharHilite(exact.getLine() + 1,
exact.getColumn() + 1);
emitCharacter(exact, handler);
handler.endCharHilite();
previousLocation = exact.next();
if (exactIter.hasNext()) {
exact = exactIter.next();
} else {
exact = null;
}
} else if (rangeStart != null) { // range start first?
emitContent(previousLocation, rangeStart, handler);
handler.startRange(rangeLoc.getLine() + 1,
rangeLoc.getColumn() + 1);
previousLocation = rangeStart;
rangeStart = null;
} else { // range end first?
emitContent(previousLocation, rangeEnd, handler);
handler.endRange();
previousLocation = rangeEnd;
if (rangeIter.hasNext()) {
Range r = rangeIter.next();
rangeStart = r.getStart();
rangeEnd = r.getEnd();
rangeLoc = r.getLoc();
} else {
rangeEnd = null;
}
}
}
emitContent(previousLocation, new Location(this, lines.size(), 0),
handler);
} finally {
handler.endSource();
}
}
/**
* Returns the uri.
*
* @return the uri
*/
public String getUri() {
return uri;
}
/**
* Returns the locationRecorder. The returned object is guaranteed to also
* implement <code>LexicalHandler</code>.
*
* @return the locationRecorder
*/
public ContentHandler getLocationRecorder() {
return locationRecorder;
}
public Location newLocatorLocation(int oneBasedLine, int oneBasedColumn) {
return new Location(this, oneBasedLine - 1, oneBasedColumn - 1);
}
}