1+ package ng .adaptor .jetty ;
2+
3+ import java .io .ByteArrayOutputStream ;
4+ import java .io .IOException ;
5+ import java .io .InputStream ;
6+ import java .io .OutputStream ;
7+ import java .io .UncheckedIOException ;
8+ import java .net .BindException ;
9+ import java .util .ArrayList ;
10+ import java .util .HashMap ;
11+ import java .util .List ;
12+ import java .util .Map ;
13+ import java .util .Map .Entry ;
14+
15+ import org .eclipse .jetty .http .HttpCookie ;
16+ import org .eclipse .jetty .http .HttpCookie .SameSite ;
17+ import org .eclipse .jetty .http .HttpField ;
18+ import org .eclipse .jetty .http .HttpFields ;
19+ import org .eclipse .jetty .io .Content ;
20+ import org .eclipse .jetty .server .Handler ;
21+ import org .eclipse .jetty .server .HttpConfiguration ;
22+ import org .eclipse .jetty .server .HttpConnectionFactory ;
23+ import org .eclipse .jetty .server .Request ;
24+ import org .eclipse .jetty .server .Response ;
25+ import org .eclipse .jetty .server .Server ;
26+ import org .eclipse .jetty .server .ServerConnector ;
27+ import org .eclipse .jetty .util .Callback ;
28+ import org .eclipse .jetty .util .Fields ;
29+ import org .eclipse .jetty .util .Fields .Field ;
30+ import org .slf4j .Logger ;
31+ import org .slf4j .LoggerFactory ;
32+
33+ import ng .appserver .NGAdaptor ;
34+ import ng .appserver .NGApplication ;
35+ import ng .appserver .NGCookie ;
36+ import ng .appserver .NGRequest ;
37+ import ng .appserver .NGResponse ;
38+ import ng .appserver .privates .NGDevelopmentInstanceStopper ;
39+
40+ public class NGAdaptorJetty extends NGAdaptor {
41+
42+ private static final Logger logger = LoggerFactory .getLogger ( NGAdaptorJetty .class );
43+
44+ private NGApplication _application ;
45+
46+ @ Override
47+ public void start ( NGApplication application ) {
48+ _application = application ;
49+
50+ final int port = 1200 ;
51+
52+ final Server server = new Server ();
53+
54+ final HttpConfiguration http = new HttpConfiguration ();
55+ final HttpConnectionFactory http11 = new HttpConnectionFactory ( http );
56+
57+ final ServerConnector connector = new ServerConnector ( server , http11 );
58+ connector .setPort ( port );
59+ server .addConnector ( connector );
60+
61+ server .setHandler ( new NGHandler () );
62+
63+ try {
64+ server .start ();
65+ server .join ();
66+ }
67+ catch ( final Exception e ) {
68+ if ( application .isDevelopmentMode () && e instanceof IOException && e .getCause () instanceof BindException ) {
69+ logger .info ( "Our port seems to be in use and we're in development mode. Let's try murdering the bastard that's blocking us" );
70+ NGDevelopmentInstanceStopper .stopPreviousDevelopmentInstance ( port );
71+ start ( application );
72+ }
73+ else {
74+ // FIXME: Handle this a bit more gracefully perhaps? // Hugi 2021-11-20
75+ e .printStackTrace ();
76+ System .exit ( -1 );
77+ }
78+ }
79+ }
80+
81+ public class NGHandler extends Handler .Abstract {
82+
83+ @ Override
84+ public boolean handle ( Request request , Response response , Callback callback ) throws Exception {
85+ doRequest ( request , response , callback );
86+ return true ;
87+ }
88+
89+ private void doRequest ( final Request jettyRequest , final Response jettyResponse , Callback callback ) throws IOException {
90+
91+ // This is where the application logic will perform it's actual work
92+ final NGRequest woRequest = requestToNGRequest ( jettyRequest );
93+ final NGResponse ngResponse = _application .dispatchRequest ( woRequest );
94+
95+ jettyResponse .setStatus ( ngResponse .status () );
96+
97+ // FIXME: Thoughts on content-length:
98+ // - Should we always be setting the content length to zero?
99+ // - Should we complain if a content stream has been set, but contentInputStreamLength not?
100+ // Hugi 2023-01-26
101+ final long contentLength ;
102+
103+ if ( ngResponse .contentInputStream () != null ) {
104+ // If an inputstream is present, use the stream's manually specified length value
105+ contentLength = ngResponse .contentInputStreamLength ();
106+ }
107+ else {
108+ // Otherwise we go for the length of the response's contained data/bytes.
109+ contentLength = ngResponse .contentBytesLength ();
110+ }
111+
112+ jettyResponse .getHeaders ().add ( "content-length" , String .valueOf ( contentLength ) );
113+
114+ for ( final NGCookie ngCookie : ngResponse .cookies () ) {
115+ Response .addCookie ( jettyResponse , ngCookieToJettyCookie ( ngCookie ) );
116+ }
117+
118+ for ( final Entry <String , List <String >> entry : ngResponse .headers ().entrySet () ) {
119+ for ( final String headerValue : entry .getValue () ) {
120+ jettyResponse .getHeaders ().add ( entry .getKey (), headerValue );
121+ }
122+ }
123+
124+ try ( final OutputStream out = Content .Sink .asOutputStream ( jettyResponse )) {
125+ if ( ngResponse .contentInputStream () != null ) {
126+ try ( final InputStream inputStream = ngResponse .contentInputStream ()) {
127+ inputStream .transferTo ( out );
128+ }
129+ }
130+ else {
131+ ngResponse .contentByteStream ().writeTo ( out );
132+ }
133+
134+ // FIXME: I'm doing this to mark the response as completed. Probably not the right way // Hugi 2024-04-05
135+ Content .Sink .write ( jettyResponse , true , "" , callback );
136+ }
137+ // if( ngResponse.contentInputStream() != null ) {
138+ // jettyResponse.write( isFailed(), null, callback );
139+ // try( final InputStream inputStream = ngResponse.contentInputStream()) {
140+ // final byte[] bytes = ngResponse.contentInputStream().readAllBytes();
141+ // jettyResponse.write( true, ByteBuffer.wrap( bytes ), callback );
142+ // }
143+ // }
144+ // else {
145+ // final byte[] bytes = ngResponse.contentByteStream().toByteArray();
146+ // jettyResponse.write( true, ByteBuffer.wrap( bytes ), callback );
147+ // }
148+ }
149+
150+ private static HttpCookie ngCookieToJettyCookie ( final NGCookie ngCookie ) {
151+ final HttpCookie .Builder jettyCookieBuilder = HttpCookie .build ( ngCookie .name (), ngCookie .value () );
152+
153+ if ( ngCookie .domain () != null ) {
154+ jettyCookieBuilder .domain ( ngCookie .domain () );
155+ }
156+
157+ if ( ngCookie .path () != null ) {
158+ jettyCookieBuilder .path ( ngCookie .path () );
159+ }
160+
161+ jettyCookieBuilder .httpOnly ( ngCookie .isHttpOnly () );
162+ jettyCookieBuilder .secure ( ngCookie .isSecure () );
163+
164+ if ( ngCookie .maxAge () != null ) {
165+ jettyCookieBuilder .maxAge ( ngCookie .maxAge () );
166+ }
167+
168+ if ( ngCookie .sameSite () != null ) {
169+ jettyCookieBuilder .sameSite ( SameSite .from ( ngCookie .sameSite () ) );
170+ }
171+
172+ return jettyCookieBuilder .build ();
173+ }
174+
175+ /**
176+ * @return the given HttpServletRequest converted to an NGRequest
177+ */
178+ private static NGRequest requestToNGRequest ( final Request sr ) {
179+
180+ // We read the formValues map before reading the requests content stream, since consuming the content stream will remove POST parameters
181+ Map <String , List <String >> formValuesFromServletRequest ;
182+ try {
183+ formValuesFromServletRequest = formValues ( Request .getParameters ( sr ) );
184+ }
185+ catch ( Exception e ) {
186+ throw new RuntimeException ( e );
187+ }
188+
189+ final ByteArrayOutputStream bos = new ByteArrayOutputStream ();
190+
191+ try ( final InputStream is = Request .asInputStream ( sr )) {
192+ is .transferTo ( bos );
193+ }
194+ catch ( final IOException e ) {
195+ throw new UncheckedIOException ( "Failed to consume the HTTP request's inputstream" , e );
196+ }
197+
198+ // FIXME: Get the protocol
199+ final NGRequest request = new NGRequest ( sr .getMethod (), sr .getHttpURI ().getCanonicalPath (), "FIXME" , headerMap ( sr ), bos .toByteArray () );
200+
201+ // FIXME: Form value parsing should really happen within the request object, not in the adaptor // Hugi 2021-12-31
202+ request ._setFormValues ( formValuesFromServletRequest );
203+
204+ // FIXME: Cookie parsing should happen within the request object, not in the adaptor // Hugi 2021-12-31
205+ request ._setCookieValues ( cookieValues ( Request .getCookies ( sr ) ) );
206+
207+ return request ;
208+ }
209+
210+ /*
211+ private static void logMultipartRequest( HttpServletRequest sr ) {
212+ // FIXME: Starting work on multipart request handling. Very much experimental/work in progress // Hugi 2023-04-16
213+ if( sr.getContentType() != null && sr.getContentType().startsWith( "multipart/form-data" ) ) {
214+ System.out.println( ">>>>>>>>>> Multipart request detected" );
215+
216+ try {
217+ // final String string = Files.createTempFile( UUID.randomUUID().toString(), ".fileupload" ).toString();
218+ // System.out.println( "Multipart temp dir: " + string );
219+
220+ for( Part part : sr.getParts() ) {
221+ // MultiPart mp = (MultiPart)part;
222+ System.out.println( "============= START PART =============" );
223+ System.out.println( "class: " + part.getClass() );
224+ System.out.println( "name: " + part.getName() );
225+ System.out.println( "contentType: " + part.getContentType() );
226+ System.out.println( "submittedFilename: " + part.getSubmittedFileName() );
227+ System.out.println( "size: " + part.getSize() );
228+ System.out.println( "value: " + new String( part.getInputStream().readAllBytes() ) );
229+
230+ System.out.println( "- Headers:" );
231+ for( String headerName : part.getHeaderNames() ) {
232+ System.out.println( "-- %s : %s".formatted( headerName, part.getHeaders( headerName ) ) );
233+
234+ }
235+
236+ System.out.println( "============= END PART =============" );
237+ }
238+ }
239+ catch( IOException e ) {
240+ throw new RuntimeException( e );
241+ }
242+ }
243+ }
244+ */
245+
246+ /**
247+ * @return The queryParameters as a formValue Map (our format)
248+ */
249+ private static Map <String , List <String >> formValues ( final Fields queryParameters ) {
250+
251+ Map <String , List <String >> map = new HashMap <>();
252+
253+ for ( Field entry : queryParameters ) {
254+ map .put ( entry .getName (), entry .getValues () );
255+ }
256+
257+ return map ;
258+ }
259+
260+ /**
261+ * @return The listed cookies as a map
262+ */
263+ private static Map <String , List <String >> cookieValues ( final List <HttpCookie > cookies ) {
264+ final Map <String , List <String >> cookieValues = new HashMap <>();
265+
266+ if ( cookies != null ) {
267+ for ( HttpCookie cookie : cookies ) {
268+ List <String > list = cookieValues .get ( cookie .getName () );
269+
270+ if ( list == null ) {
271+ list = new ArrayList <>();
272+ cookieValues .put ( cookie .getName (), list );
273+ }
274+
275+ list .add ( cookie .getValue () );
276+ }
277+ }
278+
279+ return cookieValues ;
280+ }
281+
282+ /**
283+ * @return The headers from the ServletRequest as a Map
284+ */
285+ private static Map <String , List <String >> headerMap ( final Request servletRequest ) {
286+ final Map <String , List <String >> map = new HashMap <>();
287+
288+ final HttpFields headerNamesEnumeration = servletRequest .getHeaders ();
289+
290+ for ( final HttpField httpField : headerNamesEnumeration ) {
291+ map .put ( httpField .getName (), httpField .getValueList () );
292+ }
293+
294+ return map ;
295+ }
296+ }
297+ }
0 commit comments