001/****************************************************************
002 * Licensed to the Apache Software Foundation (ASF) under one   *
003 * or more contributor license agreements.  See the NOTICE file *
004 * distributed with this work for additional information        *
005 * regarding copyright ownership.  The ASF licenses this file   *
006 * to you under the Apache License, Version 2.0 (the            *
007 * "License"); you may not use this file except in compliance   *
008 * with the License.  You may obtain a copy of the License at   *
009 *                                                              *
010 *   http://www.apache.org/licenses/LICENSE-2.0                 *
011 *                                                              *
012 * Unless required by applicable law or agreed to in writing,   *
013 * software distributed under the License is distributed on an  *
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015 * KIND, either express or implied.  See the License for the    *
016 * specific language governing permissions and limitations      *
017 * under the License.                                           *
018 ****************************************************************/
019
020package org.apache.james.mpt.session;
021
022import java.io.IOException;
023import java.nio.ByteBuffer;
024import java.nio.channels.SocketChannel;
025import java.nio.charset.Charset;
026import java.util.concurrent.Callable;
027import java.util.concurrent.TimeUnit;
028
029import org.apache.commons.lang.NotImplementedException;
030import org.apache.commons.lang.mutable.MutableInt;
031import org.apache.james.mpt.api.Monitor;
032import org.apache.james.mpt.api.Session;
033
034import com.jayway.awaitility.Awaitility;
035import com.jayway.awaitility.Duration;
036
037public final class ExternalSession implements Session {
038
039    private static final byte[] CRLF = { '\r', '\n' };
040
041    private final SocketChannel socket;
042
043    private final Monitor monitor;
044
045    private final ByteBuffer readBuffer;
046
047    private final Charset ascii;
048
049    private final ByteBuffer lineEndBuffer;
050
051    private boolean first = true;
052
053    private final String shabang;
054
055    public ExternalSession(SocketChannel socket, Monitor monitor, String shabang) {
056        this(socket, monitor, shabang, false);
057    }
058
059    public ExternalSession(SocketChannel socket, Monitor monitor, String shabang, boolean debug) {
060        super();
061        this.socket = socket;
062        this.monitor = monitor;
063        readBuffer = ByteBuffer.allocateDirect(2048);
064        ascii = Charset.forName("US-ASCII");
065        lineEndBuffer = ByteBuffer.wrap(CRLF);
066        this.shabang = shabang;
067    }
068
069    public String readLine() throws Exception {
070        StringBuffer buffer = new StringBuffer();
071        readlineInto(buffer);
072        final String result;
073        if (first && shabang != null) {
074            // fake shabang
075            monitor.note("<-" + buffer.toString());
076            result = shabang;
077            first = false;
078        }
079        else {
080            result = buffer.toString();
081            monitor.note("<-" + result);
082        }
083        return result;
084    }
085
086    private void readlineInto(StringBuffer buffer) throws Exception {
087        monitor.debug("[Reading line]");
088        readBuffer.flip();
089        while (oneFromLine(buffer))
090            ;
091        // May have partial read
092        readBuffer.compact();
093        monitor.debug("[Done]");
094    }
095
096    private boolean oneFromLine(StringBuffer buffer) throws Exception {
097        final boolean result;
098        if (readBuffer.hasRemaining()) {
099            char next = (char) readBuffer.get();
100            if (next == '\n') {
101                monitor.debug("[LF]");
102                // Reached end of the line
103                result = false;
104            }
105            else if (next == '\r') {
106                // CRLF line endings so drop
107                monitor.debug("[CR]");
108                result = true;
109            }
110            else {
111                // Load buffer
112                monitor.debug(next);
113                buffer.append(next);
114                result = true;
115            }
116        }
117        else {
118            monitor.debug("[Reading into buffer]");
119            readBuffer.clear();
120            result = tryReadFromSocket();
121            // Reset for transfer into string buffer
122            readBuffer.flip();
123            monitor.debug(String.format("[Read %d characters]", readBuffer.limit()));
124        }
125        return result;
126    }
127
128    private boolean tryReadFromSocket() throws IOException, InterruptedException {
129        final MutableInt status = new MutableInt(0);
130        Awaitility
131            .waitAtMost(Duration.ONE_MINUTE)
132            .pollDelay(new Duration(10, TimeUnit.MILLISECONDS))
133            .until(new Callable<Boolean>() {
134                @Override
135                public Boolean call() throws Exception {
136                    int read = socket.read(readBuffer);
137                    status.setValue(read);
138                    return read != 0;
139                }
140            });
141        if (status.intValue() == -1) {
142            monitor.debug("Error reading, got -1");
143            return false;
144        }
145        return true;
146    }
147
148    public void start() throws Exception {
149        while (!socket.finishConnect()) {
150            monitor.note("connecting...");
151            Thread.sleep(10);
152        }
153    }
154
155    public void restart() throws Exception {
156        throw new NotImplementedException("Restart is not implemented for ExternalSession");
157    }
158
159    public void stop() throws Exception {
160        monitor.note("closing");
161        socket.close();
162    }
163
164    public void writeLine(String line) throws Exception {
165        monitor.note("-> " + line);
166        monitor.debug("[Writing line]");
167        ByteBuffer writeBuffer = ascii.encode(line);
168        while (writeBuffer.hasRemaining()) {
169            socket.write(writeBuffer);
170        }
171        lineEndBuffer.rewind();
172        while (lineEndBuffer.hasRemaining()) {
173            socket.write(lineEndBuffer);
174        }
175        monitor.debug("[Done]");
176    }
177
178    /**
179     * Constructs a <code>String</code> with all attributes in name = value
180     * format.
181     * 
182     * @return a <code>String</code> representation of this object.
183     */
184    public String toString() {
185        final String TAB = " ";
186
187        return "External ( " + "socket = " + this.socket + TAB + "monitor = " + this.monitor + TAB
188                + "readBuffer = " + this.readBuffer + TAB + "ascii = " + this.ascii + TAB + "lineEndBuffer = "
189                + this.lineEndBuffer + TAB + "first = " + this.first + TAB + "shabang = " + this.shabang + TAB + " )";
190    }
191
192}