001/*
002 * This file is part of the JDrupes non-blocking HTTP Codec
003 * Copyright (C) 2016, 2017  Michael N. Lipp
004 *
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Lesser General Public License as published
007 * by the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
013 * License for more details.
014 *
015 * You should have received a copy of the GNU Lesser General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.httpcodec.protocols.http.client;
020
021import java.io.IOException;
022import java.io.Writer;
023import java.net.URI;
024import java.net.URISyntaxException;
025import java.nio.Buffer;
026import java.util.Objects;
027import java.util.Optional;
028import java.util.ServiceLoader;
029import java.util.stream.StreamSupport;
030
031import org.jdrupes.httpcodec.plugin.UpgradeProvider;
032import org.jdrupes.httpcodec.protocols.http.HttpConstants.HttpProtocol;
033import org.jdrupes.httpcodec.protocols.http.HttpEncoder;
034import org.jdrupes.httpcodec.protocols.http.HttpField;
035import org.jdrupes.httpcodec.protocols.http.HttpRequest;
036import org.jdrupes.httpcodec.protocols.http.HttpResponse;
037import org.jdrupes.httpcodec.types.Converters;
038import org.jdrupes.httpcodec.types.StringList;
039
040/**
041 * An encoder for HTTP requests. It accepts a header and optional
042 * payload data and encodes it into a sequence of {@link Buffer}s.
043 * 
044 * ![HttpRequestEncoder](httprequestencoder.svg)
045 * 
046 * @startuml httprequestencoder.svg
047 * class HttpRequestEncoder {
048 *      +HttpRequestEncoder(Engine engine)
049 * }
050 * 
051 * class HttpEncoder<T extends HttpMessageHeader> {
052 * }
053 * 
054 * HttpEncoder <|-- HttpRequestEncoder : <<bind>> <T -> HttpRequest>
055 *
056 */
057public class HttpRequestEncoder
058        extends HttpEncoder<HttpRequest, HttpResponse> {
059
060    private static Result.Factory resultFactory = new Result.Factory() {
061    };
062
063    /*
064     * (non-Javadoc)
065     * 
066     * @see org.jdrupes.httpcodec.Encoder#encoding()
067     */
068    @Override
069    public Class<HttpRequest> encoding() {
070        return HttpRequest.class;
071    }
072
073    /*
074     * (non-Javadoc)
075     * 
076     * @see org.jdrupes.httpcodec.protocols.http.HttpEncoder#resultFactory()
077     */
078    @Override
079    protected Result.Factory resultFactory() {
080        return resultFactory;
081    }
082
083    /*
084     * (non-Javadoc)
085     * 
086     * @see HttpEncoder#encode(HttpMessageHeader)
087     */
088    @Override
089    public void encode(HttpRequest messageHeader) {
090        if (messageHeader.protocol().equals(HttpProtocol.HTTP_1_1)) {
091            // Make sure we have a Host field, RFC 7230 5.4
092            if (!messageHeader.findStringValue(HttpField.HOST).isPresent()) {
093                messageHeader.setField(HttpField.HOST, messageHeader.host()
094                    + (messageHeader.port() < 0 ? ""
095                        : (":" + messageHeader.port())));
096            }
097        }
098        messageHeader.findField(HttpField.UPGRADE, Converters.STRING_LIST)
099            .ifPresent(field -> prepareUpgrade(field, messageHeader));
100        super.encode(messageHeader);
101    }
102
103    private void prepareUpgrade(
104            HttpField<StringList> field, HttpRequest request) {
105        if (field.value().isEmpty()) {
106            throw new IllegalArgumentException(
107                "Upgrade header field must have a value.");
108        }
109        String protocol = field.value().get(0);
110        // Load every time to support dynamic deployment of additional
111        // services in an OSGi environment.
112        Optional<UpgradeProvider> protocolPlugin = StreamSupport.stream(
113            ServiceLoader.load(UpgradeProvider.class).spliterator(), false)
114            .filter(p -> p.supportsProtocol(protocol))
115            .findFirst();
116        if (!protocolPlugin.isPresent()) {
117            // Not supported, maybe transparent to HTTP
118            return;
119        }
120        protocolPlugin.get().augmentInitialRequest(request);
121    }
122
123    /**
124     * Writes the 
125     * [request line](https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1).
126     */
127    @Override
128    protected void startMessage(HttpRequest messageHeader, Writer writer)
129            throws IOException {
130        writer.write(messageHeader.method());
131        writer.write(" ");
132        URI req = messageHeader.requestUri();
133        Optional<String> hostHdr
134            = messageHeader.findStringValue(HttpField.HOST);
135        // https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1
136        if (hostHdr.isPresent() && Objects.equals(hostHdr.get(),
137            Optional.ofNullable(req.getHost()).orElse("")
138                + ((req.getPort() < 0) ? "" : (":" + req.getPort())))
139            && req.getUserInfo() == null) {
140            try {
141                req = new URI(null, null, null, -1,
142                    req.getPath().isEmpty() ? "/" : req.getPath(),
143                    req.getQuery(), null);
144            } catch (URISyntaxException e) {
145                // Shouldn't happen, well in case it does, use original.
146            }
147        } else if (req.getFragment() != null) {
148            // https://datatracker.ietf.org/doc/html/rfc7230#section-5.1
149            try {
150                req = new URI(req.getScheme(), req.getUserInfo(),
151                    req.getHost(), req.getPort(), req.getPath(),
152                    req.getQuery(), null);
153            } catch (URISyntaxException e) {
154                // Shouldn't happen, well in case it does, use original.
155            }
156        }
157        writer.write(req.toString());
158        writer.write(" ");
159        writer.write(messageHeader.protocol().toString());
160        writer.write("\r\n");
161    }
162
163    /**
164     * Results from {@link HttpRequestEncoder} add no additional
165     * information to 
166     * {@link org.jdrupes.httpcodec.protocols.http.HttpEncoder.Result}. This
167     * class just provides a factory for creating concrete results.
168     */
169    public static class Result extends HttpEncoder.Result {
170
171        protected Result(boolean overflow, boolean underflow,
172                boolean closeConnection) {
173            super(overflow, underflow, closeConnection);
174        }
175
176        /**
177         * A concrete factory for creating new Results.
178         */
179        protected static class Factory extends HttpEncoder.Result.Factory {
180        }
181    }
182}