blob: b977ed1d75ed7fd1037ef596b3622bbdee0e9a5e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2017 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.help.internal.webapp.servlet;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.help.internal.search.HTMLDocParser;
/**
* This class replaces PLUGINS_ROOT with a relative path to eliminate redirects.
* It also performs preprocessing to add child links at runtime.
*/
public class PluginsRootResolvingStream extends OutputStream {
protected OutputStream out;
private int state = INITIAL_STATE;
private int charsMatched = 0;
private int lastKeywordMatch = 0;
private static final int INITIAL_STATE = 0;
private static final int IN_TAG = 1;
private static final int IN_QUOTE = 2;
private static final int IN_QUOTE_NOT_PLUGINS_ROOT = 3;
private static final int MAY_BE_INCLUDE = 4;
private static final int IN_METATAG = 5;
private static final String PLUGINS_ROOT = "PLUGINS_ROOT/"; //$NON-NLS-1$
private static final String INSERT_CHILD_LINKS = "<!--INSERT_CHILD_LINKS-->"; //$NON-NLS-1$
private static final String INSERT_CHILD_LINK_STYLE = "<!--INSERT_CHILD_LINK_STYLE-->"; //$NON-NLS-1$
private final String[] keywords = { INSERT_CHILD_LINKS, INSERT_CHILD_LINK_STYLE };
private boolean[] possibleKeywordMatches;
private String pathPrefix;
private StringBuilder tag;
private ByteArrayOutputStream metaTagBuffer;
private boolean tagRead;
private HttpServletRequest req;
private String charset;
public PluginsRootResolvingStream(OutputStream out, HttpServletRequest req, String prefix) {
this.out = out;
this.pathPrefix = prefix;
this.req = req;
}
@Override
public void write(int b) throws IOException {
switch(state) {
case INITIAL_STATE:
if (b == '<') {
state = IN_TAG;
charsMatched = 0;
tag = new StringBuilder();
tagRead = false;
} else {
out.write(b);
}
break;
case IN_TAG:
if (charsMatched == 0) {
if (b == '!') {
state = MAY_BE_INCLUDE;
possibleKeywordMatches = new boolean[keywords.length];
for (int i = 0; i < possibleKeywordMatches.length; i++) {
possibleKeywordMatches[i] = true;
}
charsMatched = 2; // Chars matched in "<!--INCLUDE"
lastKeywordMatch = 0;
break;
} else {
out.write('<');
}
}
if (b == '>') {
state = INITIAL_STATE;
} else if (b == '"') {
state = IN_QUOTE;
charsMatched = 0;
} else {
charsMatched++;
if (!tagRead) {
if (b >= 0 && b < 128 && tag.length() < 20) {
// ASCII
char c = (char)b;
if (Character.isLetter(c)) {
tag.append(c);
} else if (Character.isWhitespace(c)) {
tagRead = true;
if (tag.toString().equalsIgnoreCase("meta")) { //$NON-NLS-1$
state = IN_METATAG;
metaTagBuffer = new ByteArrayOutputStream(7);
metaTagBuffer.write("<meta ".getBytes()); //$NON-NLS-1$
}
} else {
tag.append(c);
}
}
}
}
out.write(b);
break;
case IN_QUOTE_NOT_PLUGINS_ROOT:
if (b == '>') {
state = INITIAL_STATE;
} else if (b == '"') {
state = IN_TAG;
charsMatched = 1;
}
out.write(b);
break;
case IN_QUOTE:
// In a quote which may start with PLUGINS_ROOT
if (b == PLUGINS_ROOT.charAt(charsMatched)) {
charsMatched++;
if (charsMatched == PLUGINS_ROOT.length()) {
out.write(pathPrefix.getBytes());
state = IN_QUOTE_NOT_PLUGINS_ROOT;
}
} else {
// We just discovered that this is not "PLUGINS_ROOT/
// flush out the characters
state = IN_QUOTE_NOT_PLUGINS_ROOT;
flushPluginsRootCharacters();
out.write(b);
}
break;
case MAY_BE_INCLUDE:
// Compare against all possible keywords
boolean canStillMatch = false;
int perfectMatch = -1;
for (int i = 0; i < keywords.length; i++) {
if (possibleKeywordMatches[i]) {
if (keywords[i].charAt(charsMatched) == b) {
canStillMatch = true;
lastKeywordMatch = i;
if (keywords[i].length() == charsMatched + 1) {
perfectMatch = i;
}
} else {
possibleKeywordMatches[i] = false;
}
}
}
if (perfectMatch != -1) {
insertBasedOnKeyword(perfectMatch);
state=INITIAL_STATE;
} else if (canStillMatch) {
charsMatched++;
} else {
state = INITIAL_STATE;
flushKeywordCharacters();
out.write(b);
}
break;
case IN_METATAG:
out.write(b);
metaTagBuffer.write(b);
if (b=='>') {
parseMetaTag(metaTagBuffer);
metaTagBuffer = null;
state = INITIAL_STATE;
}
break;
default:
out.write(b);
}
}
private void parseMetaTag(ByteArrayOutputStream buffer) {
try (ByteArrayInputStream is = new ByteArrayInputStream(buffer.toByteArray())) {
String value = HTMLDocParser.getCharsetFromHTML(is);
if (value != null) {
this.charset = value;
}
} catch (IOException e) {
}
}
protected void insertBasedOnKeyword(int index) throws IOException {
if (index == 0 ) {
ChildLinkInserter inserter = new ChildLinkInserter(req, out);
inserter.addContents(getCharset());
} else {
ChildLinkInserter inserter = new ChildLinkInserter(req, out);
inserter.addStyle();
}
}
private void flushPluginsRootCharacters() throws IOException {
out.write(PLUGINS_ROOT.substring(0, charsMatched).getBytes(StandardCharsets.UTF_8));
}
private void flushKeywordCharacters() throws IOException {
String matchingCharacters = keywords[lastKeywordMatch].substring(0, charsMatched);
out.write(matchingCharacters.getBytes(StandardCharsets.UTF_8));
}
@Override
public void close() throws IOException {
if (state == IN_QUOTE) {
flushPluginsRootCharacters();
} else if (state == MAY_BE_INCLUDE) {
flushKeywordCharacters();
}
out.close();
super.close();
}
public String getCharset() {
return charset;
}
}