• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2015 The gRPC Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package io.grpc.internal;
18 
19 import static com.google.common.base.Preconditions.checkNotNull;
20 
21 import com.google.common.annotations.VisibleForTesting;
22 import com.google.common.base.Preconditions;
23 import com.google.common.base.Throwables;
24 import com.google.common.base.Verify;
25 import io.grpc.Attributes;
26 import io.grpc.EquivalentAddressGroup;
27 import io.grpc.NameResolver;
28 import io.grpc.Status;
29 import io.grpc.internal.SharedResourceHolder.Resource;
30 import java.io.IOException;
31 import java.lang.reflect.Constructor;
32 import java.net.InetAddress;
33 import java.net.InetSocketAddress;
34 import java.net.URI;
35 import java.net.UnknownHostException;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collections;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Map.Entry;
43 import java.util.Random;
44 import java.util.Set;
45 import java.util.concurrent.ExecutorService;
46 import java.util.concurrent.atomic.AtomicReference;
47 import java.util.logging.Level;
48 import java.util.logging.Logger;
49 import javax.annotation.Nullable;
50 import javax.annotation.concurrent.GuardedBy;
51 
52 /**
53  * A DNS-based {@link NameResolver}.
54  *
55  * <p>Each {@code A} or {@code AAAA} record emits an {@link EquivalentAddressGroup} in the list
56  * passed to {@link NameResolver.Listener#onAddresses(List, Attributes)}
57  *
58  * @see DnsNameResolverProvider
59  */
60 final class DnsNameResolver extends NameResolver {
61 
62   private static final Logger logger = Logger.getLogger(DnsNameResolver.class.getName());
63 
64   private static final String SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY = "clientLanguage";
65   private static final String SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY = "percentage";
66   private static final String SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY = "clientHostname";
67   private static final String SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY = "serviceConfig";
68 
69   // From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
70   static final String SERVICE_CONFIG_PREFIX = "_grpc_config=";
71   private static final Set<String> SERVICE_CONFIG_CHOICE_KEYS =
72       Collections.unmodifiableSet(
73           new HashSet<String>(
74               Arrays.asList(
75                   SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY,
76                   SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY,
77                   SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY,
78                   SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY)));
79 
80   // From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
81   private static final String SERVICE_CONFIG_NAME_PREFIX = "_grpc_config.";
82   // From https://github.com/grpc/proposal/blob/master/A5-grpclb-in-dns.md
83   private static final String GRPCLB_NAME_PREFIX = "_grpclb._tcp.";
84 
85   private static final String JNDI_PROPERTY =
86       System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_jndi", "true");
87   private static final String JNDI_LOCALHOST_PROPERTY =
88       System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_jndi_localhost", "false");
89   private static final String JNDI_SRV_PROPERTY =
90       System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_grpclb", "false");
91   private static final String JNDI_TXT_PROPERTY =
92       System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_service_config", "false");
93 
94   @VisibleForTesting
95   static boolean enableJndi = Boolean.parseBoolean(JNDI_PROPERTY);
96   @VisibleForTesting
97   static boolean enableJndiLocalhost = Boolean.parseBoolean(JNDI_LOCALHOST_PROPERTY);
98   @VisibleForTesting
99   static boolean enableSrv = Boolean.parseBoolean(JNDI_SRV_PROPERTY);
100   @VisibleForTesting
101   static boolean enableTxt = Boolean.parseBoolean(JNDI_TXT_PROPERTY);
102 
103 
104   private static final ResourceResolverFactory resourceResolverFactory =
105       getResourceResolverFactory(DnsNameResolver.class.getClassLoader());
106 
107   @VisibleForTesting
108   final ProxyDetector proxyDetector;
109 
110   /** Access through {@link #getLocalHostname}. */
111   private static String localHostname;
112 
113   private final Random random = new Random();
114 
115   private volatile AddressResolver addressResolver = JdkAddressResolver.INSTANCE;
116   private final AtomicReference<ResourceResolver> resourceResolver =
117       new AtomicReference<ResourceResolver>();
118 
119   private final String authority;
120   private final String host;
121   private final int port;
122   private final Resource<ExecutorService> executorResource;
123   @GuardedBy("this")
124   private boolean shutdown;
125   @GuardedBy("this")
126   private ExecutorService executor;
127   @GuardedBy("this")
128   private boolean resolving;
129   @GuardedBy("this")
130   private Listener listener;
131 
DnsNameResolver(@ullable String nsAuthority, String name, Attributes params, Resource<ExecutorService> executorResource, ProxyDetector proxyDetector)132   DnsNameResolver(@Nullable String nsAuthority, String name, Attributes params,
133       Resource<ExecutorService> executorResource,
134       ProxyDetector proxyDetector) {
135     // TODO: if a DNS server is provided as nsAuthority, use it.
136     // https://www.captechconsulting.com/blogs/accessing-the-dusty-corners-of-dns-with-java
137     this.executorResource = executorResource;
138     // Must prepend a "//" to the name when constructing a URI, otherwise it will be treated as an
139     // opaque URI, thus the authority and host of the resulted URI would be null.
140     URI nameUri = URI.create("//" + checkNotNull(name, "name"));
141     Preconditions.checkArgument(nameUri.getHost() != null, "Invalid DNS name: %s", name);
142     authority = Preconditions.checkNotNull(nameUri.getAuthority(),
143         "nameUri (%s) doesn't have an authority", nameUri);
144     host = nameUri.getHost();
145     if (nameUri.getPort() == -1) {
146       Integer defaultPort = params.get(NameResolver.Factory.PARAMS_DEFAULT_PORT);
147       if (defaultPort != null) {
148         port = defaultPort;
149       } else {
150         throw new IllegalArgumentException(
151             "name '" + name + "' doesn't contain a port, and default port is not set in params");
152       }
153     } else {
154       port = nameUri.getPort();
155     }
156     this.proxyDetector = proxyDetector;
157   }
158 
159   @Override
getServiceAuthority()160   public final String getServiceAuthority() {
161     return authority;
162   }
163 
164   @Override
start(Listener listener)165   public final synchronized void start(Listener listener) {
166     Preconditions.checkState(this.listener == null, "already started");
167     executor = SharedResourceHolder.get(executorResource);
168     this.listener = Preconditions.checkNotNull(listener, "listener");
169     resolve();
170   }
171 
172   @Override
refresh()173   public final synchronized void refresh() {
174     Preconditions.checkState(listener != null, "not started");
175     resolve();
176   }
177 
178   private final Runnable resolutionRunnable = new Runnable() {
179       @Override
180       public void run() {
181         Listener savedListener;
182         synchronized (DnsNameResolver.this) {
183           if (shutdown) {
184             return;
185           }
186           savedListener = listener;
187           resolving = true;
188         }
189         try {
190           InetSocketAddress destination = InetSocketAddress.createUnresolved(host, port);
191           ProxyParameters proxy;
192           try {
193             proxy = proxyDetector.proxyFor(destination);
194           } catch (IOException e) {
195             savedListener.onError(
196                 Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e));
197             return;
198           }
199           if (proxy != null) {
200             EquivalentAddressGroup server =
201                 new EquivalentAddressGroup(
202                     new ProxySocketAddress(destination, proxy));
203             savedListener.onAddresses(Collections.singletonList(server), Attributes.EMPTY);
204             return;
205           }
206 
207           ResolutionResults resolutionResults;
208           try {
209             ResourceResolver resourceResolver = null;
210             if (shouldUseJndi(enableJndi, enableJndiLocalhost, host)) {
211               resourceResolver = getResourceResolver();
212             }
213             resolutionResults =
214                 resolveAll(addressResolver, resourceResolver, enableSrv, enableTxt, host);
215           } catch (Exception e) {
216             savedListener.onError(
217                 Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e));
218             return;
219           }
220           // Each address forms an EAG
221           List<EquivalentAddressGroup> servers = new ArrayList<>();
222           for (InetAddress inetAddr : resolutionResults.addresses) {
223             servers.add(new EquivalentAddressGroup(new InetSocketAddress(inetAddr, port)));
224           }
225           servers.addAll(resolutionResults.balancerAddresses);
226 
227           Attributes.Builder attrs = Attributes.newBuilder();
228           if (!resolutionResults.txtRecords.isEmpty()) {
229             Map<String, Object> serviceConfig = null;
230             try {
231               for (Map<String, Object> possibleConfig :
232                   parseTxtResults(resolutionResults.txtRecords)) {
233                 try {
234                   serviceConfig =
235                       maybeChooseServiceConfig(possibleConfig, random, getLocalHostname());
236                 } catch (RuntimeException e) {
237                   logger.log(Level.WARNING, "Bad service config choice " + possibleConfig, e);
238                 }
239                 if (serviceConfig != null) {
240                   break;
241                 }
242               }
243             } catch (RuntimeException e) {
244               logger.log(Level.WARNING, "Can't parse service Configs", e);
245             }
246             if (serviceConfig != null) {
247               attrs.set(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG, serviceConfig);
248             }
249           } else {
250             logger.log(Level.FINE, "No TXT records found for {0}", new Object[]{host});
251           }
252           savedListener.onAddresses(servers, attrs.build());
253         } finally {
254           synchronized (DnsNameResolver.this) {
255             resolving = false;
256           }
257         }
258       }
259     };
260 
261   @GuardedBy("this")
resolve()262   private void resolve() {
263     if (resolving || shutdown) {
264       return;
265     }
266     executor.execute(resolutionRunnable);
267   }
268 
269   @Override
shutdown()270   public final synchronized void shutdown() {
271     if (shutdown) {
272       return;
273     }
274     shutdown = true;
275     if (executor != null) {
276       executor = SharedResourceHolder.release(executorResource, executor);
277     }
278   }
279 
getPort()280   final int getPort() {
281     return port;
282   }
283 
284   @VisibleForTesting
resolveAll( AddressResolver addressResolver, @Nullable ResourceResolver resourceResolver, boolean requestSrvRecords, boolean requestTxtRecords, String name)285   static ResolutionResults resolveAll(
286       AddressResolver addressResolver,
287       @Nullable ResourceResolver resourceResolver,
288       boolean requestSrvRecords,
289       boolean requestTxtRecords,
290       String name) {
291     List<? extends InetAddress> addresses = Collections.emptyList();
292     Exception addressesException = null;
293     List<EquivalentAddressGroup> balancerAddresses = Collections.emptyList();
294     Exception balancerAddressesException = null;
295     List<String> txtRecords = Collections.emptyList();
296     Exception txtRecordsException = null;
297 
298     try {
299       addresses = addressResolver.resolveAddress(name);
300     } catch (Exception e) {
301       addressesException = e;
302     }
303     if (resourceResolver != null) {
304       if (requestSrvRecords) {
305         try {
306           balancerAddresses =
307               resourceResolver.resolveSrv(addressResolver, GRPCLB_NAME_PREFIX + name);
308         } catch (Exception e) {
309           balancerAddressesException = e;
310         }
311       }
312       if (requestTxtRecords) {
313         boolean balancerLookupFailedOrNotAttempted =
314             !requestSrvRecords || balancerAddressesException != null;
315         boolean dontResolveTxt =
316             (addressesException != null) && balancerLookupFailedOrNotAttempted;
317         // Only do the TXT record lookup if one of the above address resolutions succeeded.
318         if (!dontResolveTxt) {
319           try {
320             txtRecords = resourceResolver.resolveTxt(SERVICE_CONFIG_NAME_PREFIX + name);
321           } catch (Exception e) {
322             txtRecordsException = e;
323           }
324         }
325       }
326     }
327     try {
328       if (addressesException != null
329           && (balancerAddressesException != null || balancerAddresses.isEmpty())) {
330         Throwables.throwIfUnchecked(addressesException);
331         throw new RuntimeException(addressesException);
332       }
333     } finally {
334       if (addressesException != null) {
335         logger.log(Level.FINE, "Address resolution failure", addressesException);
336       }
337       if (balancerAddressesException != null) {
338         logger.log(Level.FINE, "Balancer resolution failure", balancerAddressesException);
339       }
340       if (txtRecordsException != null) {
341         logger.log(Level.FINE, "ServiceConfig resolution failure", txtRecordsException);
342       }
343     }
344     return new ResolutionResults(addresses, txtRecords, balancerAddresses);
345   }
346 
347   @SuppressWarnings("unchecked")
348   @VisibleForTesting
parseTxtResults(List<String> txtRecords)349   static List<Map<String, Object>> parseTxtResults(List<String> txtRecords) {
350     List<Map<String, Object>> serviceConfigs = new ArrayList<Map<String, Object>>();
351     for (String txtRecord : txtRecords) {
352       if (txtRecord.startsWith(SERVICE_CONFIG_PREFIX)) {
353         List<Map<String, Object>> choices;
354         try {
355           Object rawChoices = JsonParser.parse(txtRecord.substring(SERVICE_CONFIG_PREFIX.length()));
356           if (!(rawChoices instanceof List)) {
357             throw new IOException("wrong type " + rawChoices);
358           }
359           List<Object> listChoices = (List<Object>) rawChoices;
360           for (Object obj : listChoices) {
361             if (!(obj instanceof Map)) {
362               throw new IOException("wrong element type " + rawChoices);
363             }
364           }
365           choices = (List<Map<String, Object>>) (List<?>) listChoices;
366         } catch (IOException e) {
367           logger.log(Level.WARNING, "Bad service config: " + txtRecord, e);
368           continue;
369         }
370         serviceConfigs.addAll(choices);
371       } else {
372         logger.log(Level.FINE, "Ignoring non service config {0}", new Object[]{txtRecord});
373       }
374     }
375     return serviceConfigs;
376   }
377 
378   @Nullable
getPercentageFromChoice( Map<String, Object> serviceConfigChoice)379   private static final Double getPercentageFromChoice(
380       Map<String, Object> serviceConfigChoice) {
381     if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY)) {
382       return null;
383     }
384     return ServiceConfigUtil.getDouble(serviceConfigChoice, SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY);
385   }
386 
387   @Nullable
getClientLanguagesFromChoice( Map<String, Object> serviceConfigChoice)388   private static final List<String> getClientLanguagesFromChoice(
389       Map<String, Object> serviceConfigChoice) {
390     if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY)) {
391       return null;
392     }
393     return ServiceConfigUtil.checkStringList(
394         ServiceConfigUtil.getList(serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY));
395   }
396 
397   @Nullable
getHostnamesFromChoice( Map<String, Object> serviceConfigChoice)398   private static final List<String> getHostnamesFromChoice(
399       Map<String, Object> serviceConfigChoice) {
400     if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY)) {
401       return null;
402     }
403     return ServiceConfigUtil.checkStringList(
404         ServiceConfigUtil.getList(serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY));
405   }
406 
407   /**
408    * Determines if a given Service Config choice applies, and if so, returns it.
409    *
410    * @see <a href="https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md">
411    *   Service Config in DNS</a>
412    * @param choice The service config choice.
413    * @return The service config object or {@code null} if this choice does not apply.
414    */
415   @Nullable
416   @SuppressWarnings("BetaApi") // Verify isn't all that beta
417   @VisibleForTesting
maybeChooseServiceConfig( Map<String, Object> choice, Random random, String hostname)418   static Map<String, Object> maybeChooseServiceConfig(
419       Map<String, Object> choice, Random random, String hostname) {
420     for (Entry<String, ?> entry : choice.entrySet()) {
421       Verify.verify(SERVICE_CONFIG_CHOICE_KEYS.contains(entry.getKey()), "Bad key: %s", entry);
422     }
423 
424     List<String> clientLanguages = getClientLanguagesFromChoice(choice);
425     if (clientLanguages != null && !clientLanguages.isEmpty()) {
426       boolean javaPresent = false;
427       for (String lang : clientLanguages) {
428         if ("java".equalsIgnoreCase(lang)) {
429           javaPresent = true;
430           break;
431         }
432       }
433       if (!javaPresent) {
434         return null;
435       }
436     }
437     Double percentage = getPercentageFromChoice(choice);
438     if (percentage != null) {
439       int pct = percentage.intValue();
440       Verify.verify(pct >= 0 && pct <= 100, "Bad percentage: %s", percentage);
441       if (random.nextInt(100) >= pct) {
442         return null;
443       }
444     }
445     List<String> clientHostnames = getHostnamesFromChoice(choice);
446     if (clientHostnames != null && !clientHostnames.isEmpty()) {
447       boolean hostnamePresent = false;
448       for (String clientHostname : clientHostnames) {
449         if (clientHostname.equals(hostname)) {
450           hostnamePresent = true;
451           break;
452         }
453       }
454       if (!hostnamePresent) {
455         return null;
456       }
457     }
458     return ServiceConfigUtil.getObject(choice, SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY);
459   }
460 
461   /**
462    * Describes the results from a DNS query.
463    */
464   @VisibleForTesting
465   static final class ResolutionResults {
466     final List<? extends InetAddress> addresses;
467     final List<String> txtRecords;
468     final List<EquivalentAddressGroup> balancerAddresses;
469 
ResolutionResults( List<? extends InetAddress> addresses, List<String> txtRecords, List<EquivalentAddressGroup> balancerAddresses)470     ResolutionResults(
471         List<? extends InetAddress> addresses,
472         List<String> txtRecords,
473         List<EquivalentAddressGroup> balancerAddresses) {
474       this.addresses = Collections.unmodifiableList(checkNotNull(addresses, "addresses"));
475       this.txtRecords = Collections.unmodifiableList(checkNotNull(txtRecords, "txtRecords"));
476       this.balancerAddresses =
477           Collections.unmodifiableList(checkNotNull(balancerAddresses, "balancerAddresses"));
478     }
479   }
480 
481   @VisibleForTesting
setAddressResolver(AddressResolver addressResolver)482   void setAddressResolver(AddressResolver addressResolver) {
483     this.addressResolver = addressResolver;
484   }
485 
486   /**
487    * {@link ResourceResolverFactory} is a factory for making resource resolvers.  It supports
488    * optionally checking if the factory is available.
489    */
490   interface ResourceResolverFactory {
491 
492     /**
493      * Creates a new resource resolver.  The return value is {@code null} iff
494      * {@link #unavailabilityCause()} is not null;
495      */
newResourceResolver()496     @Nullable ResourceResolver newResourceResolver();
497 
498     /**
499      * Returns the reason why the resource resolver cannot be created.  The return value is
500      * {@code null} if {@link #newResourceResolver()} is suitable for use.
501      */
unavailabilityCause()502     @Nullable Throwable unavailabilityCause();
503   }
504 
505   /**
506    * AddressResolver resolves a hostname into a list of addresses.
507    */
508   interface AddressResolver {
resolveAddress(String host)509     List<InetAddress> resolveAddress(String host) throws Exception;
510   }
511 
512   private enum JdkAddressResolver implements AddressResolver {
513     INSTANCE;
514 
515     @Override
resolveAddress(String host)516     public List<InetAddress> resolveAddress(String host) throws UnknownHostException {
517       return Collections.unmodifiableList(Arrays.asList(InetAddress.getAllByName(host)));
518     }
519   }
520 
521   /**
522    * {@link ResourceResolver} is a Dns ResourceRecord resolver.
523    */
524   interface ResourceResolver {
resolveTxt(String host)525     List<String> resolveTxt(String host) throws Exception;
526 
resolveSrv( AddressResolver addressResolver, String host)527     List<EquivalentAddressGroup> resolveSrv(
528         AddressResolver addressResolver, String host) throws Exception;
529   }
530 
531   @Nullable
getResourceResolver()532   private ResourceResolver getResourceResolver() {
533     ResourceResolver rr;
534     if ((rr = resourceResolver.get()) == null) {
535       if (resourceResolverFactory != null) {
536         assert resourceResolverFactory.unavailabilityCause() == null;
537         rr = resourceResolverFactory.newResourceResolver();
538       }
539     }
540     return rr;
541   }
542 
543   @Nullable
544   @VisibleForTesting
getResourceResolverFactory(ClassLoader loader)545   static ResourceResolverFactory getResourceResolverFactory(ClassLoader loader) {
546     Class<? extends ResourceResolverFactory> jndiClazz;
547     try {
548       jndiClazz =
549           Class.forName("io.grpc.internal.JndiResourceResolverFactory", true, loader)
550               .asSubclass(ResourceResolverFactory.class);
551     } catch (ClassNotFoundException e) {
552       logger.log(Level.FINE, "Unable to find JndiResourceResolverFactory, skipping.", e);
553       return null;
554     }
555     Constructor<? extends ResourceResolverFactory> jndiCtor;
556     try {
557       jndiCtor = jndiClazz.getConstructor();
558     } catch (Exception e) {
559       logger.log(Level.FINE, "Can't find JndiResourceResolverFactory ctor, skipping.", e);
560       return null;
561     }
562     ResourceResolverFactory rrf;
563     try {
564       rrf = jndiCtor.newInstance();
565     } catch (Exception e) {
566       logger.log(Level.FINE, "Can't construct JndiResourceResolverFactory, skipping.", e);
567       return null;
568     }
569     if (rrf.unavailabilityCause() != null) {
570       logger.log(
571           Level.FINE,
572           "JndiResourceResolverFactory not available, skipping.",
573           rrf.unavailabilityCause());
574     }
575     return rrf;
576   }
577 
getLocalHostname()578   private static String getLocalHostname() {
579     if (localHostname == null) {
580       try {
581         localHostname = InetAddress.getLocalHost().getHostName();
582       } catch (UnknownHostException e) {
583         throw new RuntimeException(e);
584       }
585     }
586     return localHostname;
587   }
588 
589   @VisibleForTesting
shouldUseJndi(boolean jndiEnabled, boolean jndiLocalhostEnabled, String target)590   static boolean shouldUseJndi(boolean jndiEnabled, boolean jndiLocalhostEnabled, String target) {
591     if (!jndiEnabled) {
592       return false;
593     }
594     if ("localhost".equalsIgnoreCase(target)) {
595       return jndiLocalhostEnabled;
596     }
597     // Check if this name looks like IPv6
598     if (target.contains(":")) {
599       return false;
600     }
601     // Check if this might be IPv4.  Such addresses have no alphabetic characters.  This also
602     // checks the target is empty.
603     boolean alldigits = true;
604     for (int i = 0; i < target.length(); i++) {
605       char c = target.charAt(i);
606       if (c != '.') {
607         alldigits &= (c >= '0' && c <= '9');
608       }
609     }
610     return !alldigits;
611   }
612 }
613