SquirrelJME/SquirrelJME

View on GitHub
modules/gcf/src/main/java/cc/squirreljme/runtime/gcf/HTTPRequestBuilder.java

Summary

Maintainability
A
0 mins
Test Coverage
// -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
// ---------------------------------------------------------------------------
// SquirrelJME
//     Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
// ---------------------------------------------------------------------------
// SquirrelJME is under the Mozilla Public License Version 2.0.
// See license.mkd for licensing and copyright information.
// ---------------------------------------------------------------------------

package cc.squirreljme.runtime.gcf;

import cc.squirreljme.runtime.cldc.SquirrelJME;
import cc.squirreljme.runtime.cldc.debug.Debugging;
import cc.squirreljme.runtime.midlet.ApplicationHandler;
import cc.squirreljme.runtime.midlet.ApplicationInterface;
import cc.squirreljme.runtime.midlet.ApplicationType;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * This is used for building HTTP requests which will build the request to use
 * for the server.
 *
 * @since 2019/05/12
 */
public final class HTTPRequestBuilder
    extends OutputStream
{
    /** The user agent SquirrelJME uses. */
    private static final String _MIDLET_USER_AGENT =
        "SquirrelJME/" + SquirrelJME.RUNTIME_VERSION + " " +
        "Configuration/CLDC-1.0 Configuration/CLDC-1.1 " +
        "Configuration/CLDC-1.8 Profile/MIDP-1.0 Profile/MIDP-2.0 " +
        "Profile/MIDP-2.1 Profile/MIDP-3.0 Profile/MEEP-8.0";
    
    /** The remote address. */
    protected final HTTPAddress address;
    
    /** The listener when the connection is closed. */
    protected final HTTPSignalListener listener;
    
    /** State tracker for connections. */
    protected final HTTPStateTracker tracker;
    
    /** Byte data output. */
    private final ByteArrayOutputStream _bytes =
        new ByteArrayOutputStream();
    
    /** Request properties. */
    private final Map<String, String> _rqprops =
        new LinkedHashMap<>();
    
    /** The connection method. */
    private String _rqmethod =
        "GET";
    
    /**
     * Initializes the request builder.
     *
     * @param __addr The remote address.
     * @param __st State tracker for HTTP connections.
     * @param __l The agent listener.
     * @throws NullPointerException On null arguments.
     * @since 2019/05/13
     */
    public HTTPRequestBuilder(HTTPAddress __addr, HTTPStateTracker __st,
        HTTPSignalListener __l)
        throws NullPointerException
    {
        if (__l == null || __st == null || __addr == null)
            throw new NullPointerException("NARG");
        
        this.listener = __l;
        this.tracker = __st;
        this.address = __addr;
    }
    
    /**
     * {@inheritDoc}
     * @since 2019/05/12
     */
    @Override
    public final void close()
        throws IOException
    {
        // Only close once
        if (this.tracker._state != HTTPState.SETUP)
            return;
        
        // Send the agent out request bytes
        this.listener.requestReady(this.getBytes());
    }
    
    /**
     * {@inheritDoc}
     * @since 2019/05/12
     */
    @Override
    public final void flush()
        throws IOException
    {
        /* {@squirreljme.error EC04 Cannot flush closed HTTP stream.} */
        if (this.tracker._state != HTTPState.SETUP)
            throw new IOException("EC04");
        
        // Note
        Debugging.todoNote("Implement HTTP Flush", new Object[] {});
    }
    
    /**
     * Builds the bytes for the request.
     *
     * @return The bytes to send down the stream.
     * @throws IOException If they could not be set.
     * @since 2019/05/13
     */
    public final byte[] getBytes()
        throws IOException
    {
        HTTPAddress address = this.address;
        
        // Build output
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream(1024))
        {
            // Get any written data
            byte[] bytes = this._bytes.toByteArray();
            
            // Print header data, note that CRLF is used everywhere regardless
            // of whether the system uses it or not
            try (PrintStream ps = new PrintStream(baos, true, "utf-8"))
            {
                // HTTP header
                ps.printf("%s %s HTTP/1.1\r\n",
                    this._rqmethod, address.file);
                
                // Add some implicit properties
                Map<String, String> rqp = new LinkedHashMap<>(this._rqprops);
                rqp.put("host", address.ipaddr.hostname);
                
                // If the user agent was specified, add to it or set one
                String ua = rqp.get("user-agent");
                rqp.put("user-agent", HTTPRequestBuilder.buildUserAgent(ua));
                
                // Is content being specified?
                if (bytes != null)
                    rqp.put("content-length", Integer.toString(bytes.length));
                
                // Write headers
                for (Map.Entry<String, String> e : rqp.entrySet())
                    ps.printf("%s: %s\r\n", e.getKey(), e.getValue());
                
                // End of header
                ps.print("\r\n");
                
                // Flush to make sure nothing is buffered
                ps.flush();
            }
            
            // Write any data
            if (bytes != null)
                baos.write(bytes, 0, bytes.length);
            
            // Build
            return baos.toByteArray();
        }
    }
    
    /**
     * Sets the request method to use.
     *
     * @param __m The method to use.
     * @throws IOException If this is not in the setup phase.
     * @throws NullPointerException If no method was specified.
     * @since 2019/05/13
     */
    public final void setRequestMethod(String __m)
        throws IOException, NullPointerException
    {
        if (__m == null)
            throw new NullPointerException("NARG");
        
        // Set
        this._rqmethod = __m.toUpperCase();
    }
    
    /**
     * Adds or replaces an existing request property, note that for multiple
     * request property specifications they need to manually be comma
     * separated.
     *
     * @param __k The request header key.
     * @param __v The value to use, {@code null} clears.
     * @throws IOException If this is not in the setup phase.
     * @throws NullPointerException If the key was null.
     * @since 2019/05/13
     */
    public final void setRequestProperty(String __k, String __v)
        throws IOException
    {
        if (__k == null)
            throw new NullPointerException("NARG");
        
        // All fields are case insensitive, so lowercase them!
        __k = __k.toLowerCase();
        
        // Clear?
        Map<String, String> rqprops = this._rqprops;
        if (__v == null)
            rqprops.remove(__k);
        
        // Otherwise add
        else
            rqprops.put(__k, __v);
    }
    
    /**
     * {@inheritDoc}
     * @since 2019/05/12
     */
    @Override
    public final void write(int __b)
        throws IOException
    {
        /* {@squirreljme.error EC05 Cannot write more HTTP data.} */
        if (this.tracker._state != HTTPState.SETUP)
            throw new IOException("EC05");
        
        // Write to bytes
        this._bytes.write(__b);
    }
        
    /**
     * {@inheritDoc}
     * @since 2019/05/13
     */
    @Override
    public final void write(byte[] __a)
        throws IOException, NullPointerException
    {
        this.write(__a, 0, __a.length);
    }
        
    /**
     * {@inheritDoc}
     * @since 2019/05/12
     */
    @Override
    public final void write(byte[] __a, int __o, int __l)
        throws IndexOutOfBoundsException, IOException, NullPointerException
    {
        // Check
        if (__a == null)
            throw new NullPointerException("NARG");
        if (__o < 0 || __l < 0 || (__o + __l) < 0 || (__o + __l) > __a.length)
            throw new IndexOutOfBoundsException("IOOB");
        
        /* {@squirreljme.error EC06 Cannot write more HTTP data.} */
        if (this.tracker._state != HTTPState.SETUP)
            throw new IOException("EC06");
        
        // Write to bytes
        this._bytes.write(__a, __o, __l);
    }
    
    /**
     * Builds a user agent string.
     * 
     * @param __existing The existing agent, is optional.
     * @return The resultant user agent.
     * @since 2022/07/21
     */
    public static String buildUserAgent(String __existing)
    {
        // This really depends on our current interface
        ApplicationInterface<?> appInterface =
            ApplicationHandler.currentInterface();
        ApplicationType appType = (appInterface == null ? null :
            appInterface.type());
        
        // Depends on the application type
        StringBuilder result = new StringBuilder(32);
        switch ((appType == null ? ApplicationType.MIDLET : appType))
        {
                // NTT Docomo
                // https://web.archive.org/web/20090523102511/
                // http://www.nttdocomo.co.jp/service/imode/make/content/
                // browser/browser2/useragent/index.html
                // Example: "DoCoMo/2.0 DEVICE(c500;TB;W24H16)"
                // SquirrelJME gives: "DoCoMo/2.0 SJME0M3R0(c999;TJ)"
            case NTT_DOCOMO_DOJA:
            case NTT_DOCOMO_STAR:
                // Always starts with this
                result.append("DoCoMo/2.0 ");
                
                // Model number of the phone
                result.append("SJME");
                result.append(SquirrelJME.MAJOR_VERSION);
                result.append("M");
                result.append(SquirrelJME.MINOR_VERSION);
                result.append("R");
                result.append(SquirrelJME.RELEASE_VERSION);
                
                // Start of model details
                result.append('(');
                
                // Cache: 999KiB, is limited to 3 bytes?
                result.append("c999");
                
                // How is this being accessed?
                result.append(';');
                if (appType == ApplicationType.NTT_DOCOMO_STAR)
                    result.append("SJ");
                else
                    result.append("TJ");
                
                // The "W24H16" is the size of the terminal display in
                // characters for when a browser is displaying a page, it is
                // only used when the mode is TB or TC. Six bytes only so
                // limited to 99x99.
                
                // End of model details
                result.append(')');
                break;
            
                // Midlet as a default
            default:
                if (__existing != null)
                {
                    result.append(__existing);
                    result.append(' ');
                }
                
                // Append default agent
                result.append(HTTPRequestBuilder._MIDLET_USER_AGENT);
                break;
        }
        
        return result.toString();
    }
}