1414
1515package com .google .cloud .functions .invoker .runner ;
1616
17+ import static java .util .stream .Collectors .toList ;
18+
1719import com .beust .jcommander .JCommander ;
1820import com .beust .jcommander .Parameter ;
1921import com .beust .jcommander .ParameterException ;
2830import com .google .cloud .functions .invoker .NewHttpFunctionExecutor ;
2931import java .io .File ;
3032import java .io .IOException ;
33+ import java .io .UncheckedIOException ;
3134import java .lang .reflect .Method ;
35+ import java .net .MalformedURLException ;
3236import java .net .URL ;
3337import java .net .URLClassLoader ;
3438import java .nio .file .Files ;
3539import java .nio .file .Path ;
3640import java .nio .file .Paths ;
41+ import java .util .ArrayList ;
3742import java .util .Arrays ;
43+ import java .util .Collections ;
44+ import java .util .List ;
3845import java .util .Map ;
3946import java .util .Objects ;
4047import java .util .Optional ;
@@ -91,13 +98,18 @@ private static class Options {
9198 System .getenv ().getOrDefault ("FUNCTION_TARGET" , "TestFunction.function" );
9299
93100 @ Parameter (
94- description = "Name of a jar file that contains the function to execute. This must be"
95- + " self-contained: either it must be a \" fat jar\" which bundles the dependencies"
96- + " of all of the function code, or it must use the Class-Path attribute in the jar"
97- + " manifest to point to those dependencies." ,
98- names = "--jar"
101+ description = "List of files or directories where the compiled Java classes making up"
102+ + " the function will be found. This functions like the -classpath option to the"
103+ + " java command. It is a list of filenames separated by '${path.separator}'."
104+ + " If an entry in the list names a directory then the class foo.bar.Baz will be looked"
105+ + " for in foo${file.separator}bar${file.separator}Baz.class under that"
106+ + " directory. If an entry in the list names a file and that file is a jar file then"
107+ + " class foo.bar.Baz will be looked for in an entry foo/bar/Baz.class in that jar"
108+ + " file. If an entry is a directory followed by '${file.separator}*' then every file"
109+ + " in the directory whose name ends with '.jar' will be searched for classes." ,
110+ names = "--classpath"
99111 )
100- private String jar = null ;
112+ private String classPath = null ;
101113
102114 @ Parameter (
103115 names = "--help" , help = true
@@ -124,12 +136,12 @@ static Optional<Invoker> makeInvoker(Map<String, String> environment, String...
124136 try {
125137 jCommander .parse (args );
126138 } catch (ParameterException e ) {
127- jCommander . usage ();
139+ usage (jCommander );
128140 throw e ;
129141 }
130142
131143 if (options .help ) {
132- jCommander . usage ();
144+ usage (jCommander );
133145 return Optional .empty ();
134146 }
135147
@@ -138,15 +150,15 @@ static Optional<Invoker> makeInvoker(Map<String, String> environment, String...
138150 port = Integer .parseInt (options .port );
139151 } catch (NumberFormatException e ) {
140152 System .err .println ("--port value should be an integer: " + options .port );
141- jCommander . usage ();
153+ usage (jCommander );
142154 throw e ;
143155 }
144156 String functionTarget = options .target ;
145157 Path standardFunctionJarPath = Paths .get ("function/function.jar" );
146- Optional <String > functionJarPath =
158+ Optional <String > functionClasspath =
147159 Arrays .asList (
148- options .jar ,
149- environment .get ("FUNCTION_JAR " ),
160+ options .classPath ,
161+ environment .get ("FUNCTION_CLASSPATH " ),
150162 Files .exists (standardFunctionJarPath ) ? standardFunctionJarPath .toString () : null )
151163 .stream ()
152164 .filter (Objects ::nonNull )
@@ -156,28 +168,37 @@ static Optional<Invoker> makeInvoker(Map<String, String> environment, String...
156168 port ,
157169 functionTarget ,
158170 environment .get ("FUNCTION_SIGNATURE_TYPE" ),
159- functionJarPath );
171+ functionClasspath );
160172 return Optional .of (invoker );
161173 }
162174
175+ private static void usage (JCommander jCommander ) {
176+ StringBuilder usageBuilder = new StringBuilder ();
177+ jCommander .getUsageFormatter ().usage (usageBuilder );
178+ String usage = usageBuilder .toString ()
179+ .replace ("${file.separator}" , File .separator )
180+ .replace ("${path.separator}" , File .pathSeparator );
181+ jCommander .getConsole ().println (usage );
182+ }
183+
163184 private static boolean isLocalRun () {
164185 return System .getenv ("K_SERVICE" ) == null ;
165186 }
166187
167188 private final Integer port ;
168189 private final String functionTarget ;
169190 private final String functionSignatureType ;
170- private final Optional <String > functionJarPath ;
191+ private final Optional <String > functionClasspath ;
171192
172193 public Invoker (
173194 Integer port ,
174195 String functionTarget ,
175196 String functionSignatureType ,
176- Optional <String > functionJarPath ) {
197+ Optional <String > functionClasspath ) {
177198 this .port = port ;
178199 this .functionTarget = functionTarget ;
179200 this .functionSignatureType = functionSignatureType ;
180- this .functionJarPath = functionJarPath ;
201+ this .functionClasspath = functionClasspath ;
181202 }
182203
183204 Integer getPort () {
@@ -192,8 +213,8 @@ String getFunctionSignatureType() {
192213 return functionSignatureType ;
193214 }
194215
195- Optional <String > getFunctionJarPath () {
196- return functionJarPath ;
216+ Optional <String > getFunctionClasspath () {
217+ return functionClasspath ;
197218 }
198219
199220 public void startServer () throws Exception {
@@ -203,21 +224,11 @@ public void startServer() throws Exception {
203224 context .setContextPath ("/" );
204225 server .setHandler (context );
205226
206- Optional <File > functionJarFile =
207- functionJarPath .isPresent ()
208- ? Optional .of (new File (functionJarPath .get ()))
209- : Optional .empty ();
210- if (functionJarFile .isPresent () && !functionJarFile .get ().exists ()) {
211- throw new IllegalArgumentException (
212- "functionJarPath points to an non-existent file: "
213- + functionJarFile .get ().getAbsolutePath ());
214- }
215-
216227 ClassLoader runtimeLoader = getClass ().getClassLoader ();
217228 ClassLoader classLoader ;
218- if (functionJarFile .isPresent ()) {
229+ if (functionClasspath .isPresent ()) {
219230 ClassLoader parent = new OnlyApiClassLoader (runtimeLoader );
220- classLoader = new URLClassLoader (new URL []{ functionJarFile .get (). toURI (). toURL ()} , parent );
231+ classLoader = new URLClassLoader (classpathToUrls ( functionClasspath .get ()) , parent );
221232 } else {
222233 classLoader = runtimeLoader ;
223234 }
@@ -279,6 +290,39 @@ public void startServer() throws Exception {
279290 server .join ();
280291 }
281292
293+ static URL [] classpathToUrls (String classpath ) throws IOException {
294+ String [] components = classpath .split (File .pathSeparator );
295+ List <URL > urls = new ArrayList <>();
296+ for (String component : components ) {
297+ if (component .endsWith (File .separator + "*" )) {
298+ urls .addAll (jarsIn (component .substring (0 , component .length () - 2 )));
299+ } else {
300+ Path path = Paths .get (component );
301+ if (Files .exists (path )) {
302+ urls .add (path .toUri ().toURL ());
303+ }
304+ }
305+ }
306+ return urls .toArray (new URL [0 ]);
307+ }
308+
309+ private static List <URL > jarsIn (String dir ) throws IOException {
310+ Path path = Paths .get (dir );
311+ if (!Files .isDirectory (path )) {
312+ return Collections .emptyList ();
313+ }
314+ return Files .list (path )
315+ .filter (p -> p .getFileName ().toString ().endsWith (".jar" ))
316+ .map (p -> {
317+ try {
318+ return p .toUri ().toURL ();
319+ } catch (MalformedURLException e ) {
320+ throw new UncheckedIOException (e );
321+ }
322+ })
323+ .collect (toList ());
324+ }
325+
282326 private void logServerInfo () {
283327 if (isLocalRun ()) {
284328 logger .log (Level .INFO , "Serving function..." );
0 commit comments