55
66import com .google .api .client .json .JsonFactory ;
77import com .google .api .services .cloudresourcemanager .model .Project ;
8+ import com .google .common .base .Function ;
89import com .google .common .base .Joiner ;
910import com .google .common .base .Objects ;
11+ import com .google .common .base .Predicates ;
1012import com .google .common .collect .ImmutableList ;
13+ import com .google .common .collect .ImmutableMap ;
14+ import com .google .common .collect .ImmutableSet ;
15+ import com .google .common .collect .Iterables ;
16+ import com .google .common .collect .Lists ;
1117import com .google .common .io .ByteStreams ;
1218
1319import com .sun .net .httpserver .Headers ;
2026import java .io .IOException ;
2127import java .io .InputStream ;
2228import java .io .OutputStream ;
29+ import java .net .HttpURLConnection ;
2330import java .net .InetSocketAddress ;
2431import java .net .ServerSocket ;
2532import java .nio .charset .StandardCharsets ;
26- import java .util .ArrayList ;
2733import java .util .Date ;
2834import java .util .HashMap ;
2935import java .util .List ;
3036import java .util .Map ;
3137import java .util .Random ;
38+ import java .util .Set ;
39+ import java .util .concurrent .ConcurrentHashMap ;
3240import java .util .logging .Logger ;
3341import java .util .zip .GZIPInputStream ;
3442
@@ -42,11 +50,14 @@ public class LocalResourceManagerHelper {
4250 private static final Logger log = Logger .getLogger (LocalResourceManagerHelper .class .getName ());
4351 private static final JsonFactory jsonFactory =
4452 new com .google .api .client .json .jackson .JacksonFactory ();
45- private static final int HTTP_STATUS_OK = 200 ;
4653 private static final Random PROJECT_NUMBER_GENERATOR = new Random ();
4754
55+ // see https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects
56+ private static final Set <Character > PERMISSIBLE_PROJECT_NAME_PUNCTUATION =
57+ ImmutableSet .of ('-' , '\'' , '"' , ' ' , '!' );
58+
4859 private HttpServer server ;
49- private final Map <String , Project > projects = new HashMap <>();
60+ private final ConcurrentHashMap <String , Project > projects = new ConcurrentHashMap <>();
5061
5162 static class Response {
5263 private final int code ;
@@ -96,7 +107,7 @@ private static String toJson(
96107 args .put ("message" , message );
97108 args .put ("status" , status );
98109 try {
99- return jsonFactory .toString (args );
110+ return jsonFactory .toString (ImmutableMap . of ( "error" , args ) );
100111 } catch (IOException e ) {
101112 throw new RuntimeException ("Error when generating JSON error response." );
102113 }
@@ -131,6 +142,7 @@ public void handle(HttpExchange exchange) throws IOException {
131142 if (response == null ) {
132143 throw new UnsupportedOperationException ("Request not recognized." );
133144 }
145+ exchange .getResponseHeaders ().set ("Content-type" , "application/json; charset=UTF-8" );
134146 exchange .sendResponseHeaders (response .code (), response .body ().length ());
135147 OutputStream outputStream = exchange .getResponseBody ();
136148 outputStream .write (response .body ().getBytes ());
@@ -173,6 +185,9 @@ private static Map<String, Object> parseListOptions(String query) {
173185 case "fields" :
174186 options .put ("fields" , argEntry [1 ].split ("," ));
175187 break ;
188+ case "filter" :
189+ options .put ("filter" , argEntry [1 ].split (" " ));
190+ break ;
176191 case "pageToken" :
177192 // support pageToken when Cloud Resource Manager supports this (#421)
178193 break ;
@@ -185,20 +200,74 @@ private static Map<String, Object> parseListOptions(String query) {
185200 return options ;
186201 }
187202
203+ private static final boolean isValidProject (Project project ) {
204+ if (project .getProjectId () == null ) {
205+ log .info ("Project ID cannot be empty." );
206+ return false ;
207+ }
208+ if (!isValidIdOrLabel (project .getProjectId (), 6 , 30 )) {
209+ log .info ("Project " + project .getProjectId () + " has an invalid ID."
210+ + " See https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects"
211+ + " for more information." );
212+ return false ;
213+ }
214+ if (project .getName () != null ) {
215+ for (char c : project .getName ().toCharArray ()) {
216+ if (!PERMISSIBLE_PROJECT_NAME_PUNCTUATION .contains (c ) && !Character .isLetterOrDigit (c )) {
217+ log .info ("Project " + project .getProjectId () + " has an invalid name."
218+ + " See https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects"
219+ + " for more information." );
220+ return false ;
221+ }
222+ }
223+ }
224+ if (project .getLabels () != null ) {
225+ if (project .getLabels ().size () > 256 ) {
226+ log .info ("Project " + project .getProjectId () + " exceeds the limit of 256 labels." );
227+ return false ;
228+ }
229+ for (Map .Entry <String , String > entry : project .getLabels ().entrySet ()) {
230+ if (!isValidIdOrLabel (entry .getKey (), 1 , 63 )
231+ || !isValidIdOrLabel (entry .getValue (), 0 , 63 )) {
232+ log .info ("Project " + project .getProjectId () + " has an invalid label entry."
233+ + " See https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects"
234+ + " for more information." );
235+ return false ;
236+ }
237+ }
238+ }
239+ return true ;
240+ }
241+
242+ private static final boolean isValidIdOrLabel (String value , int minLength , int maxLength ) {
243+ for (char c : value .toCharArray ()) {
244+ if (c != '-' && !Character .isDigit (c )
245+ && (!Character .isLetter (c ) || !Character .isLowerCase (c ))) {
246+ return false ;
247+ }
248+ }
249+ if (value .length () > 0 && (!Character .isLetter (value .charAt (0 )) || value .endsWith ("-" ))) {
250+ return false ;
251+ }
252+ return value .length () >= minLength && value .length () <= maxLength ;
253+ }
254+
188255 Response create (Project project ) throws IOException {
189256 project .setLifecycleState ("ACTIVE" );
190257 project .setProjectNumber (Math .abs (PROJECT_NUMBER_GENERATOR .nextLong ()));
191258 project .setCreateTime (ISODateTimeFormat .dateTime ().print (new Date ().getTime ()));
192259 Response response ;
193- if (projects .containsKey (checkNotNull (project .getProjectId ()))) {
260+ if (!isValidProject (project )) {
261+ response = Error .INVALID_ARGUMENT .response ;
262+ } else if (projects .containsKey (project .getProjectId ())) {
194263 response = Error .ALREADY_EXISTS .response ;
195264 log .info (
196265 "A project with the same project ID (" + project .getProjectId () + ") already exists." );
197266 } else {
198267 projects .put (project .getProjectId (), project );
199268 String createdProjectStr = jsonFactory .toString (project );
200269 log .info ("Created the following project: " + createdProjectStr );
201- response = new Response (HTTP_STATUS_OK , createdProjectStr );
270+ response = new Response (HttpURLConnection . HTTP_OK , createdProjectStr );
202271 }
203272 return response ;
204273 }
@@ -214,7 +283,7 @@ Response delete(String projectId) {
214283 log .info ("Error when deleting " + projectId + " because the lifecycle state was not ACTIVE." );
215284 } else {
216285 project .setLifecycleState ("DELETE_REQUESTED" );
217- response = new Response (HTTP_STATUS_OK , "{}" );
286+ response = new Response (HttpURLConnection . HTTP_OK , "{}" );
218287 log .info ("Successfully requested delete for the following project: " + projectId );
219288 }
220289 return response ;
@@ -227,23 +296,69 @@ Response get(String projectId, String[] fields) throws IOException {
227296 log .info ("Project not found." );
228297 } else {
229298 response = new Response (
230- HTTP_STATUS_OK , jsonFactory .toString (extractFields (projects .get (projectId ), fields )));
299+ HttpURLConnection .HTTP_OK ,
300+ jsonFactory .toString (extractFields (projects .get (projectId ), fields )));
231301 }
232302 return response ;
233303 }
234304
235- Response list (Map <String , Object > options ) throws IOException {
305+ Response list (final Map <String , Object > options ) {
236306 // Use pageSize and pageToken options when Cloud Resource Manager does so (#421)
237- List <String > projectsSerialized = new ArrayList <>();
238- for (Project p : projects .values ()) {
239- projectsSerialized .add (
240- jsonFactory .toString (extractFields (p , (String []) options .get ("fields" ))));
241- }
307+ List <String > projectsSerialized = Lists .newArrayList (Iterables .filter (
308+ Iterables .transform (projects .values (), new Function <Project , String >() {
309+ @ Override
310+ public String apply (Project p ) {
311+ try {
312+ return includeProject (p , (String []) options .get ("filter" ))
313+ ? jsonFactory .toString (extractFields (p , (String []) options .get ("fields" ))) : null ;
314+ } catch (IOException e ) {
315+ log .info ("Error when serializing project " + p .getProjectId ());
316+ return null ;
317+ }
318+ }
319+ }),
320+ Predicates .notNull ()));
242321 StringBuilder responseBody = new StringBuilder ();
243322 responseBody .append ("{\" projects\" : [" );
244323 responseBody .append (Joiner .on ("," ).join (projectsSerialized ));
245324 responseBody .append ("]}" );
246- return new Response (HTTP_STATUS_OK , responseBody .toString ());
325+ return new Response (HttpURLConnection .HTTP_OK , responseBody .toString ());
326+ }
327+
328+ private static boolean includeProject (Project project , String [] filters ) {
329+ if (filters == null ) {
330+ return true ;
331+ }
332+ for (String filter : filters ) {
333+ String [] filterEntry = filter .toLowerCase ().split (":" );
334+ if ("id" .equals (filterEntry [0 ])) {
335+ if (!satisfiesFilter (project .getProjectId (), filterEntry [1 ])) {
336+ return false ;
337+ }
338+ } else if ("name" .equals (filterEntry [0 ])) {
339+ if (!satisfiesFilter (project .getName (), filterEntry [1 ])) {
340+ return false ;
341+ }
342+ } else if (filterEntry [0 ].startsWith ("labels" )) {
343+ String labelKey = filterEntry [0 ].split ("\\ ." )[1 ];
344+ if (project .getLabels () != null ) {
345+ String labelValue = project .getLabels ().get (labelKey );
346+ if (!satisfiesFilter (labelValue , filterEntry [1 ])) {
347+ return false ;
348+ }
349+ }
350+ } else {
351+ log .info ("Could not parse the following filter: " + filter );
352+ }
353+ }
354+ return true ;
355+ }
356+
357+ private static boolean satisfiesFilter (String projectValue , String filterValue ) {
358+ if (projectValue == null ) {
359+ return false ;
360+ }
361+ return "*" .equals (filterValue ) ? true : filterValue .equals (projectValue .toLowerCase ());
247362 }
248363
249364 private static Project extractFields (Project fullProject , String [] fields ) {
@@ -281,7 +396,7 @@ private static Project extractFields(Project fullProject, String[] fields) {
281396
282397 Response replace (Project project ) throws IOException {
283398 Response response ;
284- Project oldProject = projects .get (checkNotNull ( project .getProjectId () ));
399+ Project oldProject = projects .get (project .getProjectId ());
285400 if (oldProject == null ) {
286401 response = Error .PERMISSION_DENIED .response ; // when possible, change this to 404 (#440)
287402 log .info (
@@ -302,7 +417,7 @@ Response replace(Project project) throws IOException {
302417 projects .put (project .getProjectId (), project );
303418 String updatedProjectStr = jsonFactory .toString (project );
304419 log .info ("Successfully updated the project to be: " + updatedProjectStr );
305- response = new Response (HTTP_STATUS_OK , updatedProjectStr );
420+ response = new Response (HttpURLConnection . HTTP_OK , updatedProjectStr );
306421 }
307422 return response ;
308423 }
@@ -319,7 +434,7 @@ Response undelete(String projectId) {
319434 + " because the lifecycle state was not DELETE_REQUESTED." );
320435 } else {
321436 project .setLifecycleState ("ACTIVE" );
322- response = new Response (HTTP_STATUS_OK , "{}" );
437+ response = new Response (HttpURLConnection . HTTP_OK , "{}" );
323438 log .info ("Successfully undeleted " + projectId + "." );
324439 }
325440 return response ;
@@ -378,11 +493,10 @@ public void stop() {
378493 * @return true if the project was successfully added, false otherwise
379494 */
380495 public boolean addProject (Project project ) {
381- if (projects . containsKey ( checkNotNull ( project . getProjectId ()) )) {
382- return false ;
496+ if (isValidProject ( project )) {
497+ return projects . putIfAbsent ( project . getProjectId (), clone ( project )) == null ? true : false ;
383498 }
384- projects .put (project .getProjectId (), clone (project ));
385- return true ;
499+ return false ;
386500 }
387501
388502 /**
@@ -391,22 +505,20 @@ public boolean addProject(Project project) {
391505 * @return Project (if it exists) or null (if it doesn't exist)
392506 */
393507 public Project getProject (String projectId ) {
394- com . google . api . services . cloudresourcemanager . model . Project original = projects .get (projectId );
508+ Project original = projects .get (projectId );
395509 return original != null ? clone (projects .get (projectId )) : null ;
396510 }
397511
398- private static com .google .api .services .cloudresourcemanager .model .Project clone (
399- com .google .api .services .cloudresourcemanager .model .Project original ) {
400- com .google .api .services .cloudresourcemanager .model .Project clone =
401- new com .google .api .services .cloudresourcemanager .model .Project ();
402- clone .setProjectId (original .getProjectId ());
403- clone .setName (original .getName ());
404- clone .setLabels (original .getLabels ());
405- clone .setProjectNumber (original .getProjectNumber ());
406- clone .setCreateTime (original .getCreateTime ());
407- clone .setLifecycleState (original .getLifecycleState ());
408- clone .setParent (original .getParent ());
409- return clone ;
512+ private static Project clone (Project original ) {
513+ return new Project ()
514+ .setProjectId (original .getProjectId ())
515+ .setName (original .getName ())
516+ .setLabels (original .getLabels () != null ? ImmutableMap .copyOf (original .getLabels ()) : null )
517+ .setProjectNumber (
518+ original .getProjectNumber () != null ? original .getProjectNumber ().longValue () : null )
519+ .setCreateTime (original .getCreateTime ())
520+ .setLifecycleState (original .getLifecycleState ())
521+ .setParent (original .getParent () != null ? original .getParent ().clone () : null );
410522 }
411523
412524 /**
@@ -418,11 +530,7 @@ private static com.google.api.services.cloudresourcemanager.model.Project clone(
418530 * @return true if the project was successfully deleted, false otherwise.
419531 */
420532 public boolean removeProject (String projectId ) {
421- if (projects .containsKey (projectId )) {
422- projects .remove (checkNotNull (projectId ));
423- return true ;
424- }
425- return false ;
533+ return projects .remove (checkNotNull (projectId )) != null ? true : false ;
426534 }
427535
428536 /**
@@ -431,8 +539,7 @@ public boolean removeProject(String projectId) {
431539 * @return true if the project number was successfully changed, false otherwise.
432540 */
433541 public boolean changeProjectNumber (String projectId , long projectNumber ) {
434- com .google .api .services .cloudresourcemanager .model .Project project =
435- projects .get (checkNotNull (projectId ));
542+ Project project = projects .get (checkNotNull (projectId ));
436543 if (project != null ) {
437544 project .setProjectNumber (projectNumber );
438545 return true ;
@@ -450,8 +557,7 @@ public boolean changeLifecycleState(String projectId, String lifecycleState) {
450557 "ACTIVE" .equals (lifecycleState ) || "DELETE_REQUESTED" .equals (lifecycleState )
451558 || "DELETE_IN_PROGRESS" .equals (lifecycleState ),
452559 "Lifecycle state must be ACTIVE, DELETE_REQUESTED, or DELETE_IN_PROGRESS" );
453- com .google .api .services .cloudresourcemanager .model .Project project =
454- projects .get (checkNotNull (projectId ));
560+ Project project = projects .get (checkNotNull (projectId ));
455561 if (project != null ) {
456562 project .setLifecycleState (lifecycleState );
457563 return true ;
@@ -465,10 +571,9 @@ public boolean changeLifecycleState(String projectId, String lifecycleState) {
465571 * @return true if the project create time was successfully changed, false otherwise.
466572 */
467573 public boolean changeCreateTime (String projectId , String createTime ) {
468- com .google .api .services .cloudresourcemanager .model .Project project =
469- projects .get (checkNotNull (projectId ));
574+ Project project = projects .get (checkNotNull (projectId ));
470575 if (project != null ) {
471- project .setCreateTime (createTime );
576+ project .setCreateTime (checkNotNull ( createTime ) );
472577 return true ;
473578 }
474579 return false ;
0 commit comments