blob: 85258fbb77cd71b52365f2ffa4b8a1b77facec9e [file] [log] [blame]
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF 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 *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* 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.apache.james.mime4j.parser;
import java.io.IOException;
import java.util.BitSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.james.mime4j.BodyDescriptor;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.MutableBodyDescriptor;
import org.apache.james.mime4j.descriptor.DefaultBodyDescriptor;
import org.apache.james.mime4j.descriptor.MaximalBodyDescriptor;
import org.apache.james.mime4j.stream.BufferingInputStream;
import org.apache.james.mime4j.util.ByteArrayBuffer;
import org.apache.james.mime4j.util.CharArrayBuffer;
import org.apache.james.mime4j.util.MessageUtils;
/**
* Abstract MIME entity.
*/
public abstract class AbstractEntity implements EntityStateMachine {
protected final Log log;
protected final BodyDescriptor parent;
protected final int startState;
protected final int endState;
protected final boolean maximalBodyDescriptor;
protected final boolean strictParsing;
protected final MutableBodyDescriptor body;
protected int state;
private final ByteArrayBuffer linebuf;
private final CharArrayBuffer fieldbuf;
private int lineCount;
private String field, fieldName, fieldValue;
private boolean endOfHeader;
private static final BitSet fieldChars = new BitSet();
static {
for (int i = 0x21; i <= 0x39; i++) {
fieldChars.set(i);
}
for (int i = 0x3b; i <= 0x7e; i++) {
fieldChars.set(i);
}
}
/**
* Internal state, not exposed.
*/
private static final int T_IN_BODYPART = -2;
/**
* Internal state, not exposed.
*/
private static final int T_IN_MESSAGE = -3;
AbstractEntity(
BodyDescriptor parent,
int startState,
int endState,
boolean maximalBodyDescriptor,
boolean strictParsing) {
this.log = LogFactory.getLog(getClass());
this.parent = parent;
this.state = startState;
this.startState = startState;
this.endState = endState;
this.maximalBodyDescriptor = maximalBodyDescriptor;
this.strictParsing = strictParsing;
this.body = newBodyDescriptor(parent);
this.linebuf = new ByteArrayBuffer(64);
this.fieldbuf = new CharArrayBuffer(64);
this.lineCount = 0;
this.endOfHeader = false;
}
public int getState() {
return state;
}
/**
* Creates a new instance of {@link BodyDescriptor}. Subclasses may override
* this in order to create body descriptors, that provide more specific
* information.
*/
protected MutableBodyDescriptor newBodyDescriptor(BodyDescriptor pParent) {
final MutableBodyDescriptor result;
if (maximalBodyDescriptor) {
result = new MaximalBodyDescriptor(pParent);
} else {
result = new DefaultBodyDescriptor(pParent);
}
return result;
}
protected abstract int getLineNumber();
protected abstract BufferingInputStream getDataStream();
private void fillFieldBuffer() throws IOException, MimeException {
if (endOfHeader) {
return;
}
BufferingInputStream instream = getDataStream();
fieldbuf.clear();
for (;;) {
// If there's still data stuck in the line buffer
// copy it to the field buffer
int len = linebuf.length();
if (len > 0) {
fieldbuf.append(linebuf, 0, len);
}
linebuf.clear();
if (instream.readLine(linebuf) == -1) {
monitor(Event.HEADERS_PREMATURE_END);
endOfHeader = true;
break;
}
len = linebuf.length();
if (len > 0 && linebuf.byteAt(len - 1) == '\n') {
len--;
}
if (len > 0 && linebuf.byteAt(len - 1) == '\r') {
len--;
}
if (len == 0) {
// empty line detected
endOfHeader = true;
break;
}
lineCount++;
if (lineCount > 1) {
int ch = linebuf.byteAt(0);
if (ch != MessageUtils.SP && ch != MessageUtils.HT) {
// new header detected
break;
}
}
}
}
protected boolean parseField() throws IOException {
for (;;) {
if (endOfHeader) {
return false;
}
fillFieldBuffer();
// Strip away line delimiter
int len = fieldbuf.length();
if (len > 0 && fieldbuf.charAt(len - 1) == '\n') {
len--;
}
if (len > 0 && fieldbuf.charAt(len - 1) == '\r') {
len--;
}
fieldbuf.setLength(len);
boolean valid = true;
field = fieldbuf.toString();
int pos = fieldbuf.indexOf(':');
if (pos == -1) {
monitor(Event.INALID_HEADER);
valid = false;
} else {
fieldName = fieldbuf.substring(0, pos);
for (int i = 0; i < fieldName.length(); i++) {
if (!fieldChars.get(fieldName.charAt(i))) {
monitor(Event.INALID_HEADER);
valid = false;
break;
}
}
fieldValue = fieldbuf.substring(pos + 1, fieldbuf.length());
}
if (valid) {
body.addField(fieldName, fieldValue);
return true;
}
}
}
/**
* <p>Gets a descriptor for the current entity.
* This method is valid if {@link #getState()} returns:</p>
* <ul>
* <li>{@link #T_BODY}</li>
* <li>{@link #T_START_MULTIPART}</li>
* <li>{@link #T_EPILOGUE}</li>
* <li>{@link #T_PREAMBLE}</li>
* </ul>
* @return <code>BodyDescriptor</code>, not nulls
*/
public BodyDescriptor getBodyDescriptor() {
switch (getState()) {
case EntityStates.T_BODY:
case EntityStates.T_START_MULTIPART:
case EntityStates.T_PREAMBLE:
case EntityStates.T_EPILOGUE:
case EntityStates.T_END_OF_STREAM:
return body;
default:
throw new IllegalStateException("Invalid state :" + stateToString(state));
}
}
/**
* This method is valid, if {@link #getState()} returns {@link #T_FIELD}.
* @return String with the fields raw contents.
* @throws IllegalStateException {@link #getState()} returns another
* value than {@link #T_FIELD}.
*/
public String getField() {
switch (getState()) {
case EntityStates.T_FIELD:
return field;
default:
throw new IllegalStateException("Invalid state :" + stateToString(state));
}
}
/**
* This method is valid, if {@link #getState()} returns {@link #T_FIELD}.
* @return String with the fields name.
* @throws IllegalStateException {@link #getState()} returns another
* value than {@link #T_FIELD}.
*/
public String getFieldName() {
switch (getState()) {
case EntityStates.T_FIELD:
return fieldName;
default:
throw new IllegalStateException("Invalid state :" + stateToString(state));
}
}
/**
* This method is valid, if {@link #getState()} returns {@link #T_FIELD}.
* @return String with the fields value.
* @throws IllegalStateException {@link #getState()} returns another
* value than {@link #T_FIELD}.
*/
public String getFieldValue() {
switch (getState()) {
case EntityStates.T_FIELD:
return fieldValue;
default:
throw new IllegalStateException("Invalid state :" + stateToString(state));
}
}
/**
* Monitors the given event.
* Subclasses may override to perform actions upon events.
* Base implementation logs at warn.
* @param event <code>Event</code>, not null
* @throws MimeException subclasses may elect to throw this exception upon
* invalid content
* @throws IOException subclasses may elect to throw this exception
*/
protected void monitor(Event event) throws MimeException, IOException {
if (strictParsing) {
throw new MimeParseEventException(event);
} else {
warn(event);
}
}
/**
* Creates an indicative message suitable for display
* based on the given event and the current state of the system.
* @param event <code>Event</code>, not null
* @return message suitable for use as a message in an exception
* or for logging
*/
protected String message(Event event) {
String preamble = "Line " + getLineNumber() + ": ";
final String message;
if (event == null) {
message = "Event is unexpectedly null.";
} else {
message = event.toString();
}
final String result = preamble + message;
return result;
}
/**
* Logs (at warn) an indicative message based on the given event
* and the current state of the system.
* @param event <code>Event</code>, not null
*/
protected void warn(Event event) {
if (log.isWarnEnabled()) {
log.warn(message(event));
}
}
/**
* Logs (at debug) an indicative message based on the given event
* and the current state of the system.
* @param event <code>Event</code>, not null
*/
protected void debug(Event event) {
if (log.isDebugEnabled()) {
log.debug(message(event));
}
}
public String toString() {
return getClass().getName() + " [" + stateToString(state)
+ "][" + body.getMimeType() + "][" + body.getBoundary() + "]";
}
/**
* Renders a state as a string suitable for logging.
* @param state
* @return rendered as string, not null
*/
public static final String stateToString(int state) {
final String result;
switch (state) {
case EntityStates.T_END_OF_STREAM:
result = "End of stream";
break;
case EntityStates.T_START_MESSAGE:
result = "Start message";
break;
case EntityStates.T_END_MESSAGE:
result = "End message";
break;
case EntityStates.T_RAW_ENTITY:
result = "Raw entity";
break;
case EntityStates.T_START_HEADER:
result = "Start header";
break;
case EntityStates.T_FIELD:
result = "Field";
break;
case EntityStates.T_END_HEADER:
result = "End header";
break;
case EntityStates.T_START_MULTIPART:
result = "Start multipart";
break;
case EntityStates.T_END_MULTIPART:
result = "End multipart";
break;
case EntityStates.T_PREAMBLE:
result = "Premable";
break;
case EntityStates.T_EPILOGUE:
result = "Epilogue";
break;
case EntityStates.T_START_BODYPART:
result = "Start bodypart";
break;
case EntityStates.T_END_BODYPART:
result = "End bodypart";
break;
case EntityStates.T_BODY:
result = "Body";
break;
case T_IN_BODYPART:
result = "Bodypart";
break;
case T_IN_MESSAGE:
result = "In message";
break;
default:
result = "Unknown";
break;
}
return result;
}
}