Skip to content

Commit a976033

Browse files
Added the experimental plain Jetty adaptor
1 parent 547138e commit a976033

File tree

2 files changed

+297
-10
lines changed

2 files changed

+297
-10
lines changed

ng-adaptor-jetty/pom.xml

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,5 @@
2222
<artifactId>jetty-server</artifactId>
2323
<version>12.0.19</version>
2424
</dependency>
25-
<dependency>
26-
<groupId>org.eclipse.jetty.ee10</groupId>
27-
<artifactId>jetty-ee10-servlet</artifactId>
28-
<version>12.0.19</version>
29-
</dependency>
30-
<dependency>
31-
<groupId>org.eclipse.jetty.ee10.websocket</groupId>
32-
<artifactId>jetty-ee10-websocket-jakarta-server</artifactId>
33-
<version>12.0.19</version>
34-
</dependency>
3525
</dependencies>
3626
</project>
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
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

Comments
 (0)