Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / admin / AppYamlTranslator.java
blob29ffaade11ff6389a8a644f517f1c15428f99513
1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.admin;
5 import com.google.appengine.tools.info.Version;
6 import com.google.apphosting.utils.config.AppEngineConfigException;
7 import com.google.apphosting.utils.config.AppEngineWebXml;
8 import com.google.apphosting.utils.config.AppEngineWebXml.AdminConsolePage;
9 import com.google.apphosting.utils.config.AppEngineWebXml.ApiConfig;
10 import com.google.apphosting.utils.config.AppEngineWebXml.ErrorHandler;
11 import com.google.apphosting.utils.config.AppEngineWebXml.Pagespeed;
12 import com.google.apphosting.utils.config.BackendsXml;
13 import com.google.apphosting.utils.config.WebXml;
14 import com.google.apphosting.utils.config.WebXml.SecurityConstraint;
15 import com.google.apphosting.utils.glob.ConflictResolver;
16 import com.google.apphosting.utils.glob.Glob;
17 import com.google.apphosting.utils.glob.GlobFactory;
18 import com.google.apphosting.utils.glob.GlobIntersector;
19 import com.google.apphosting.utils.glob.LongestPatternConflictResolver;
20 import com.google.common.collect.Maps;
22 import net.sourceforge.yamlbeans.YamlConfig;
23 import net.sourceforge.yamlbeans.YamlException;
24 import net.sourceforge.yamlbeans.YamlWriter;
26 import java.io.StringWriter;
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Set;
35 /**
36 * Generates {@code app.yaml} files suitable for uploading as part of
37 * a Google App Engine application.
40 public class AppYamlTranslator {
41 private static final String NO_API_VERSION = "none";
43 private static final ConflictResolver RESOLVER =
44 new LongestPatternConflictResolver();
46 private static final String DYNAMIC_PROPERTY = "dynamic";
47 private static final String STATIC_PROPERTY = "static";
48 private static final String WELCOME_FILES = "welcome";
49 private static final String TRANSPORT_GUARANTEE_PROPERTY = "transportGuarantee";
50 private static final String REQUIRED_ROLE_PROPERTY = "requiredRole";
51 private static final String EXPIRATION_PROPERTY = "expiration";
52 private static final String HTTP_HEADERS_PROPERTY = "http_headers";
53 private static final String API_ENDPOINT_REGEX = "/_ah/spi/*";
55 private static final String[] PROPERTIES = new String[] {
56 DYNAMIC_PROPERTY,
57 STATIC_PROPERTY,
58 WELCOME_FILES,
59 TRANSPORT_GUARANTEE_PROPERTY,
60 REQUIRED_ROLE_PROPERTY,
61 EXPIRATION_PROPERTY,
64 private static final int MAX_HANDLERS = 100;
66 private final AppEngineWebXml appEngineWebXml;
67 private final WebXml webXml;
68 private final BackendsXml backendsXml;
69 private final String apiVersion;
70 private final Set<String> staticFiles;
71 private final String runtime;
72 private final Version sdkVersion;
74 public AppYamlTranslator(AppEngineWebXml appEngineWebXml,
75 WebXml webXml,
76 BackendsXml backendsXml,
77 String apiVersion,
78 Set<String> staticFiles,
79 ApiConfig apiConfig,
80 String runtime,
81 Version sdkVersion) {
82 this.appEngineWebXml = appEngineWebXml;
83 this.webXml = webXml;
84 this.backendsXml = backendsXml;
85 this.apiVersion = apiVersion;
86 this.staticFiles = staticFiles;
87 this.runtime = runtime;
88 this.sdkVersion = sdkVersion;
91 public String getYaml() {
92 StringBuilder builder = new StringBuilder();
93 translateAppEngineWebXml(builder);
94 translateApiVersion(builder);
95 translateWebXml(builder);
96 return builder.toString();
99 private void appendIfNotNull(StringBuilder builder, String tag, Object value) {
100 if (value != null) {
101 builder.append(tag);
102 builder.append(value);
103 builder.append("\n");
107 private void translateAppEngineWebXml(StringBuilder builder) {
108 builder.append("application: '" + appEngineWebXml.getAppId() + "'\n");
109 builder.append("runtime: " + runtime + "\n");
110 if (appEngineWebXml.getUseVm()) {
111 builder.append("vm: True\n");
114 if (appEngineWebXml.getSourceLanguage() != null) {
115 builder.append("source_language: '" + appEngineWebXml.getSourceLanguage() + "'\n");
118 if (appEngineWebXml.getMajorVersionId() != null) {
119 builder.append("version: '" + appEngineWebXml.getMajorVersionId() + "'\n");
122 if (appEngineWebXml.getModule() != null) {
123 builder.append("module: '" + appEngineWebXml.getModule() + "'\n");
126 if (appEngineWebXml.getInstanceClass() != null) {
127 builder.append("instance_class: " + appEngineWebXml.getInstanceClass() + "\n");
130 if (!appEngineWebXml.getAutomaticScaling().isEmpty()) {
131 builder.append("automatic_scaling:\n");
132 AppEngineWebXml.AutomaticScaling settings = appEngineWebXml.getAutomaticScaling();
133 appendIfNotNull(builder, " min_pending_latency: ", settings.getMinPendingLatency());
134 appendIfNotNull(builder, " max_pending_latency: ", settings.getMaxPendingLatency());
135 appendIfNotNull(builder, " min_idle_instances: ", settings.getMinIdleInstances());
136 appendIfNotNull(builder, " max_idle_instances: ", settings.getMaxIdleInstances());
137 appendIfNotNull(builder, " max_concurrent_requests: ", settings.getMaxConcurrentRequests());
140 if (!appEngineWebXml.getManualScaling().isEmpty()) {
141 builder.append("manual_scaling:\n");
142 AppEngineWebXml.ManualScaling settings = appEngineWebXml.getManualScaling();
143 builder.append(" instances: " + settings.getInstances() + "\n");
146 if (!appEngineWebXml.getBasicScaling().isEmpty()) {
147 builder.append("basic_scaling:\n");
148 AppEngineWebXml.BasicScaling settings = appEngineWebXml.getBasicScaling();
149 builder.append(" max_instances: " + settings.getMaxInstances() + "\n");
150 appendIfNotNull(builder, " idle_timeout: ", settings.getIdleTimeout());
153 Collection<String> services = appEngineWebXml.getInboundServices();
154 if (!services.isEmpty()) {
155 builder.append("inbound_services:\n");
156 for (String service : services) {
157 builder.append("- " + service + "\n");
161 if (appEngineWebXml.getPrecompilationEnabled()) {
162 builder.append("derived_file_type:\n");
163 builder.append("- java_precompiled\n");
166 if (appEngineWebXml.getThreadsafe()) {
167 builder.append("threadsafe: True\n");
170 if (appEngineWebXml.getAutoIdPolicy() != null) {
171 builder.append("auto_id_policy: " + appEngineWebXml.getAutoIdPolicy() + "\n");
172 } else {
173 builder.append("auto_id_policy: default\n");
176 if (appEngineWebXml.getCodeLock()) {
177 builder.append("code_lock: True\n");
180 List<AdminConsolePage> adminConsolePages = appEngineWebXml.getAdminConsolePages();
181 if (!adminConsolePages.isEmpty()) {
182 builder.append("admin_console:\n");
183 builder.append(" pages:\n");
184 for (AdminConsolePage page : adminConsolePages) {
185 builder.append(" - name: " + page.getName() + "\n");
186 builder.append(" url: " + page.getUrl() + "\n");
190 List<ErrorHandler> errorHandlers = appEngineWebXml.getErrorHandlers();
191 if (!errorHandlers.isEmpty()) {
192 builder.append("error_handlers:\n");
193 for (ErrorHandler handler : errorHandlers) {
194 String fileName = handler.getFile();
195 if (!fileName.startsWith("/")) {
196 fileName = "/" + fileName;
198 if (!staticFiles.contains("__static__" + fileName)) {
199 throw new AppEngineConfigException("No static file found for error handler: " +
200 fileName + ", out of " + staticFiles);
202 builder.append("- file: __static__" + fileName + "\n");
203 if (handler.getErrorCode() != null) {
204 builder.append(" error_code: " + handler.getErrorCode() + "\n");
206 String mimeType = webXml.getMimeTypeForPath(handler.getFile());
207 if (mimeType != null) {
208 builder.append(" mime_type: " + mimeType + "\n");
213 if (backendsXml != null) {
214 builder.append(backendsXml.toYaml());
217 ApiConfig apiConfig = appEngineWebXml.getApiConfig();
218 if (apiConfig != null) {
219 builder.append("api_config:\n");
220 builder.append(" url: " + apiConfig.getUrl() + "\n");
221 builder.append(" script: unused\n");
224 if (appEngineWebXml.getPagespeed() != null) {
225 builder.append("pagespeed:\n");
226 appendPagespeed(appEngineWebXml.getPagespeed(), builder, 2);
229 if (appEngineWebXml.getUseVm()) {
230 appendVmSettings(appEngineWebXml.getVmSettings(), builder);
235 * Appends the given VM Settings as YAML to the given StringBuilder.
237 * @param vmSettings The vm settings map to append as YAML.
238 * @param builder The StringBuilder to append to.
240 private void appendVmSettings(Map<String, String> vmSettings, StringBuilder builder) {
241 builder.append("vm_settings:\n");
242 if (vmSettings == null || !vmSettings.containsKey("image")) {
243 if (sdkVersion != null && sdkVersion.getRelease() != null) {
244 builder.append(" 'image': " + yamlQuote(sdkVersion.getRelease()) + "\n");
247 if (vmSettings != null) {
248 for (Map.Entry<String, String> setting : vmSettings.entrySet()) {
249 builder.append(
250 " " + yamlQuote(setting.getKey()) + ": " + yamlQuote(setting.getValue()) + "\n");
256 * Append the given Pagespeed node as YAML to the given StringBuilder.
257 * @param pagespeed The Pagespeed instance to append as YAML.
258 * @param builder The StringBuilder to append to.
259 * @param indent The number of spaces to indent the pagespeed YAML.
261 public static void appendPagespeed(Pagespeed pagespeed, StringBuilder builder, int indent) {
262 if (pagespeed != null && !pagespeed.isEmpty()) {
263 Map<String, List<String>> config = Maps.newTreeMap();
264 putListInMapIfNotEmpty(config, "url_blacklist", pagespeed.getUrlBlacklist());
265 putListInMapIfNotEmpty(config, "domains_to_rewrite", pagespeed.getDomainsToRewrite());
266 putListInMapIfNotEmpty(config, "enabled_rewriters", pagespeed.getEnabledRewriters());
267 putListInMapIfNotEmpty(config, "disabled_rewriters", pagespeed.getDisabledRewriters());
268 appendObjectAsYaml(builder, config, indent);
273 * Adds the list to the given map, using the specified name, if the list is non-null and not
274 * empty.
276 private static void putListInMapIfNotEmpty(Map<String, List<String>> map, String name,
277 List<String> values) {
278 if (values != null && !values.isEmpty()) {
279 map.put(name, values);
284 * Appends the given collection to the StringBuilder as YAML, indenting each emitted line by
285 * numIndentSpaces.
287 private static void appendObjectAsYaml(
288 StringBuilder builder, Object collection, int numIndentSpaces) {
289 StringBuilder prefixBuilder = new StringBuilder();
290 for (int i = 0; i < numIndentSpaces; ++i) {
291 prefixBuilder.append(' ');
293 final String indentPrefix = prefixBuilder.toString();
295 StringWriter stringWriter = new StringWriter();
296 YamlConfig yamlConfig = new YamlConfig();
297 yamlConfig.writeConfig.setIndentSize(2);
298 yamlConfig.writeConfig.setWriteRootTags(false);
300 YamlWriter writer = new YamlWriter(stringWriter, yamlConfig);
301 try {
302 writer.write(collection);
303 writer.close();
304 } catch (YamlException e) {
305 throw new AppEngineConfigException("Unable to generate YAML.", e);
308 for (String line : stringWriter.toString().split("\n")) {
309 builder.append(indentPrefix);
310 builder.append(line);
311 builder.append("\n");
316 * Surrounds the provided string with single quotes, escaping any single
317 * quotes in the string by replacing them with ''.
319 private String yamlQuote(String str) {
320 return "'" + str.replace("'", "''") + "'";
323 private void translateApiVersion(StringBuilder builder) {
324 if (apiVersion == null) {
325 builder.append("api_version: '" + NO_API_VERSION + "'\n");
326 } else {
327 builder.append("api_version: '" + apiVersion + "'\n");
331 private void translateWebXml(StringBuilder builder) {
332 builder.append("handlers:\n");
334 AbstractHandlerGenerator staticGenerator = null;
335 if (staticFiles.isEmpty()) {
336 staticGenerator = new EmptyHandlerGenerator();
337 } else {
338 staticGenerator = new StaticHandlerGenerator(appEngineWebXml.getPublicRoot());
341 DynamicHandlerGenerator dynamicGenerator =
342 new DynamicHandlerGenerator(webXml.getFallThroughToRuntime());
343 if (staticGenerator.size() + dynamicGenerator.size() > MAX_HANDLERS) {
344 dynamicGenerator = new DynamicHandlerGenerator(true);
347 staticGenerator.translate(builder);
348 dynamicGenerator.translate(builder);
351 class StaticHandlerGenerator extends AbstractHandlerGenerator {
352 private final String root;
354 public StaticHandlerGenerator(String root) {
355 this.root = root;
358 @Override
359 protected Map<String, Object> getWelcomeProperties() {
360 List<String> staticWelcomeFiles = new ArrayList<String>();
361 for (String welcomeFile : webXml.getWelcomeFiles()) {
362 for (String staticFile : staticFiles) {
363 if (staticFile.endsWith("/" + welcomeFile)) {
364 staticWelcomeFiles.add(welcomeFile);
365 break;
369 return Collections.<String,Object>singletonMap(WELCOME_FILES, staticWelcomeFiles);
372 @Override
373 protected void addPatterns(GlobIntersector intersector) {
374 List<AppEngineWebXml.StaticFileInclude> includes = appEngineWebXml.getStaticFileIncludes();
375 if (includes.isEmpty()) {
376 intersector.addGlob(GlobFactory.createGlob("/*", STATIC_PROPERTY, true));
377 } else {
378 for (AppEngineWebXml.StaticFileInclude include : includes) {
379 String pattern = include.getPattern().replaceAll("\\*\\*", "*");
380 if (!pattern.startsWith("/")) {
381 pattern = "/" + pattern;
383 Map<String, Object> props = new HashMap<String, Object>();
384 props.put(STATIC_PROPERTY, true);
385 if (include.getExpiration() != null) {
386 props.put(EXPIRATION_PROPERTY, include.getExpiration());
388 if (include.getHttpHeaders() != null) {
389 props.put(HTTP_HEADERS_PROPERTY, include.getHttpHeaders());
392 intersector.addGlob(GlobFactory.createGlob(pattern, props));
397 @Override
398 public void translateGlob(StringBuilder builder, Glob glob) {
399 String regex = glob.getRegularExpression().pattern();
400 if (!root.equals("")) {
401 if (regex.startsWith(root)){
402 regex = regex.substring(root.length(), regex.length());
405 @SuppressWarnings("unchecked")
406 List<String> welcomeFiles =
407 (List<String>) glob.getProperty(WELCOME_FILES, RESOLVER);
408 if (welcomeFiles != null) {
409 for (String welcomeFile : welcomeFiles) {
410 builder.append("- url: (" + regex + ")\n");
411 builder.append(" static_files: __static__" + root + "\\1" + welcomeFile + "\n");
412 builder.append(" upload: __NOT_USED__\n");
413 builder.append(" require_matching_file: True\n");
414 translateHandlerOptions(builder, glob);
415 translateAdditionalStaticOptions(builder, glob);
417 } else {
418 Boolean isStatic = (Boolean) glob.getProperty(STATIC_PROPERTY, RESOLVER);
419 if (isStatic != null && isStatic.booleanValue()) {
420 builder.append("- url: (" + regex + ")\n");
421 builder.append(" static_files: __static__" + root + "\\1\n");
422 builder.append(" upload: __NOT_USED__\n");
423 builder.append(" require_matching_file: True\n");
424 translateHandlerOptions(builder, glob);
425 translateAdditionalStaticOptions(builder, glob);
430 private void translateAdditionalStaticOptions(StringBuilder builder, Glob glob)
431 throws AppEngineConfigException {
432 String expiration = (String) glob.getProperty(EXPIRATION_PROPERTY, RESOLVER);
433 if (expiration != null) {
434 builder.append(" expiration: " + expiration + "\n");
437 @SuppressWarnings("unchecked")
438 Map<String, String> httpHeaders =
439 (Map<String, String>) glob.getProperty(HTTP_HEADERS_PROPERTY, RESOLVER);
440 if (httpHeaders != null && !httpHeaders.isEmpty()) {
441 builder.append(" http_headers:\n");
442 appendObjectAsYaml(builder, httpHeaders, 4);
448 * According to the example In section 12.2.2 of Servlet Spec 3.0 , /baz/* should also match /baz,
449 * so add an additional glob for that.
451 private static void extendMeaningOfTrailingStar(
452 GlobIntersector intersector, String pattern, String property, Object value) {
453 if (pattern.endsWith("/*") && pattern.length() > 2) {
454 intersector.addGlob(
455 GlobFactory.createGlob(pattern.substring(0, pattern.length() - 2), property, value));
459 class DynamicHandlerGenerator extends AbstractHandlerGenerator {
460 private final List<String> patterns;
461 private boolean fallthrough;
462 private boolean hasJsps;
464 DynamicHandlerGenerator(boolean alwaysFallthrough) {
465 fallthrough = alwaysFallthrough;
466 patterns = new ArrayList<String>();
467 for (String servletPattern : webXml.getServletPatterns()) {
468 if (servletPattern.equals("/") || servletPattern.equals("/*")) {
469 fallthrough = true;
470 } else if (servletPattern.equals(API_ENDPOINT_REGEX)) {
471 hasApiEndpoint = true;
472 } else if (servletPattern.endsWith(".jsp")) {
473 hasJsps = true;
474 } else {
475 patterns.add(servletPattern);
480 @Override
481 protected Map<String, Object> getWelcomeProperties() {
482 if (fallthrough) {
483 return null;
484 } else {
485 return Collections.<String,Object>singletonMap(DYNAMIC_PROPERTY, true);
489 @Override
490 protected void addPatterns(GlobIntersector intersector) {
491 if (fallthrough) {
492 intersector.addGlob(GlobFactory.createGlob(
493 "/*",
494 DYNAMIC_PROPERTY, true));
495 } else {
496 for (String servletPattern : patterns) {
497 intersector.addGlob(GlobFactory.createGlob(
498 servletPattern,
499 DYNAMIC_PROPERTY, true));
500 extendMeaningOfTrailingStar(intersector, servletPattern, DYNAMIC_PROPERTY, true);
502 if (hasJsps) {
503 intersector.addGlob(GlobFactory.createGlob(
504 "*.jsp",
505 DYNAMIC_PROPERTY, true));
506 } else if (appEngineWebXml.getUseVm()) {
507 intersector.addGlob(GlobFactory.createGlob(
508 "*.jsp",
509 DYNAMIC_PROPERTY, true));
511 intersector.addGlob(GlobFactory.createGlob(
512 "/_ah/*",
513 DYNAMIC_PROPERTY, true));
517 @Override
518 public void translateGlob(StringBuilder builder, Glob glob) {
519 String regex = glob.getRegularExpression().pattern();
521 Boolean isDynamic = (Boolean) glob.getProperty(DYNAMIC_PROPERTY, RESOLVER);
522 if (isDynamic != null && isDynamic.booleanValue()) {
523 builder.append("- url: " + regex + "\n");
524 builder.append(" script: unused\n");
525 translateHandlerOptions(builder, glob);
531 * An {@code AbstractHandlerGenerator} that returns no globs.
533 class EmptyHandlerGenerator extends AbstractHandlerGenerator {
534 @Override
535 protected void addPatterns(GlobIntersector intersector) {
538 @Override
539 protected void translateGlob(StringBuilder builder, Glob glob) {
542 @Override
543 protected Map<String, Object> getWelcomeProperties() {
544 return Collections.emptyMap();
548 abstract class AbstractHandlerGenerator {
549 private List<Glob> globs = null;
550 protected boolean hasApiEndpoint;
552 public int size() {
553 return getGlobPatterns().size();
556 public void translate(StringBuilder builder) {
557 for (Glob glob : getGlobPatterns()) {
558 translateGlob(builder, glob);
562 abstract protected void addPatterns(GlobIntersector intersector);
563 abstract protected void translateGlob(StringBuilder builder, Glob glob);
566 * @returns a map of welcome properties to apply to the welcome
567 * file entries, or {@code null} if no welcome file entries are
568 * necessary.
570 abstract protected Map<String, Object> getWelcomeProperties();
572 protected List<Glob> getGlobPatterns() {
573 if (globs == null) {
574 GlobIntersector intersector = new GlobIntersector();
575 addPatterns(intersector);
576 addSecurityConstraints(intersector);
577 addWelcomeFiles(intersector);
579 globs = intersector.getIntersection();
580 removeNearDuplicates(globs);
581 if (hasApiEndpoint) {
582 globs.add(GlobFactory.createGlob(API_ENDPOINT_REGEX, DYNAMIC_PROPERTY, true));
585 return globs;
588 protected void addWelcomeFiles(GlobIntersector intersector) {
589 Map<String, Object> welcomeProperties = getWelcomeProperties();
590 if (welcomeProperties != null) {
591 intersector.addGlob(GlobFactory.createGlob("/", welcomeProperties));
592 intersector.addGlob(GlobFactory.createGlob("/*/", welcomeProperties));
596 protected void addSecurityConstraints(GlobIntersector intersector) {
597 for (SecurityConstraint constraint : webXml.getSecurityConstraints()) {
598 for (String pattern : constraint.getUrlPatterns()) {
599 intersector.addGlob(GlobFactory.createGlob(
600 pattern,
601 TRANSPORT_GUARANTEE_PROPERTY,
602 constraint.getTransportGuarantee()));
603 extendMeaningOfTrailingStar(intersector, pattern, TRANSPORT_GUARANTEE_PROPERTY,
604 constraint.getTransportGuarantee());
605 intersector.addGlob(GlobFactory.createGlob(
606 pattern,
607 REQUIRED_ROLE_PROPERTY,
608 constraint.getRequiredRole()));
609 extendMeaningOfTrailingStar(
610 intersector, pattern, REQUIRED_ROLE_PROPERTY, constraint.getRequiredRole());
615 protected void translateHandlerOptions(StringBuilder builder, Glob glob) {
616 SecurityConstraint.RequiredRole requiredRole =
617 (SecurityConstraint.RequiredRole) glob.getProperty(REQUIRED_ROLE_PROPERTY, RESOLVER);
618 if (requiredRole == null) {
619 requiredRole = SecurityConstraint.RequiredRole.NONE;
621 switch (requiredRole) {
622 case NONE:
623 builder.append(" login: optional\n");
624 break;
625 case ANY_USER:
626 builder.append(" login: required\n");
627 break;
628 case ADMIN:
629 builder.append(" login: admin\n");
630 break;
633 SecurityConstraint.TransportGuarantee transportGuarantee =
634 (SecurityConstraint.TransportGuarantee) glob.getProperty(TRANSPORT_GUARANTEE_PROPERTY,
635 RESOLVER);
636 if (transportGuarantee == null) {
637 transportGuarantee = SecurityConstraint.TransportGuarantee.NONE;
639 switch (transportGuarantee) {
640 case NONE:
641 if (appEngineWebXml.getSslEnabled()) {
642 builder.append(" secure: optional\n");
643 } else {
644 builder.append(" secure: never\n");
646 break;
647 case INTEGRAL:
648 case CONFIDENTIAL:
649 if (!appEngineWebXml.getSslEnabled()) {
650 throw new AppEngineConfigException(
651 "SSL must be enabled in appengine-web.xml to use transport-guarantee");
653 builder.append(" secure: always\n");
654 break;
657 String pattern = glob.getRegularExpression().pattern();
658 String id = webXml.getHandlerIdForPattern(pattern);
659 if (id != null) {
660 if (appEngineWebXml.isApiEndpoint(id)) {
661 builder.append(" api_endpoint: True\n");
666 private void removeNearDuplicates(List<Glob> globs) {
667 for (int i = 0; i < globs.size(); i++) {
668 Glob topGlob = globs.get(i);
669 for (int j = i + 1; j < globs.size(); j++) {
670 Glob bottomGlob = globs.get(j);
671 if (bottomGlob.matchesAll(topGlob)) {
672 if (propertiesMatch(topGlob, bottomGlob)) {
673 globs.remove(i);
674 i--;
676 break;
682 private boolean propertiesMatch(Glob glob1, Glob glob2) {
683 for (String property : PROPERTIES) {
684 Object value1 = glob1.getProperty(property, RESOLVER);
685 Object value2 = glob2.getProperty(property, RESOLVER);
686 if (value1 != value2 && (value1 == null || !value1.equals(value2))) {
687 return false;
690 return true;