/******************************************************************************* * Copyright (c) 2009, 2016 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * 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 StringBuffer 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 StringBuffer(); 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; } }