001/*
002 * Copyright 2012-2016 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.bind;
018
019import java.net.InetAddress;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.HashSet;
024import java.util.LinkedHashMap;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Map;
028import java.util.Properties;
029import java.util.Set;
030
031import org.springframework.beans.BeanWrapper;
032import org.springframework.beans.BeanWrapperImpl;
033import org.springframework.beans.BeansException;
034import org.springframework.beans.InvalidPropertyException;
035import org.springframework.beans.MutablePropertyValues;
036import org.springframework.beans.NotWritablePropertyException;
037import org.springframework.beans.PropertyValue;
038import org.springframework.core.convert.ConversionService;
039import org.springframework.core.convert.TypeDescriptor;
040import org.springframework.core.env.StandardEnvironment;
041import org.springframework.util.LinkedMultiValueMap;
042import org.springframework.util.MultiValueMap;
043import org.springframework.util.StringUtils;
044import org.springframework.validation.AbstractPropertyBindingResult;
045import org.springframework.validation.BeanPropertyBindingResult;
046import org.springframework.validation.DataBinder;
047
048/**
049 * Binder implementation that allows caller to bind to maps and also allows property names
050 * to match a bit loosely (if underscores or dashes are removed and replaced with camel
051 * case for example).
052 *
053 * @author Dave Syer
054 * @author Phillip Webb
055 * @author Stephane Nicoll
056 * @author Andy Wilkinson
057 * @see RelaxedNames
058 */
059public class RelaxedDataBinder extends DataBinder {
060
061        private static final Object BLANK = new Object();
062
063        private String namePrefix;
064
065        private boolean ignoreNestedProperties;
066
067        private MultiValueMap<String, String> nameAliases = new LinkedMultiValueMap<String, String>();
068
069        /**
070         * Create a new {@link RelaxedDataBinder} instance.
071         * @param target the target into which properties are bound
072         */
073        public RelaxedDataBinder(Object target) {
074                super(wrapTarget(target));
075        }
076
077        /**
078         * Create a new {@link RelaxedDataBinder} instance.
079         * @param target the target into which properties are bound
080         * @param namePrefix An optional prefix to be used when reading properties
081         */
082        public RelaxedDataBinder(Object target, String namePrefix) {
083                super(wrapTarget(target),
084                                (StringUtils.hasLength(namePrefix) ? namePrefix : DEFAULT_OBJECT_NAME));
085                this.namePrefix = cleanNamePrefix(namePrefix);
086        }
087
088        private String cleanNamePrefix(String namePrefix) {
089                if (!StringUtils.hasLength(namePrefix)) {
090                        return null;
091                }
092                return (namePrefix.endsWith(".") ? namePrefix : namePrefix + ".");
093        }
094
095        /**
096         * Flag to disable binding of nested properties (i.e. those with period separators in
097         * their paths). Can be useful to disable this if the name prefix is empty and you
098         * don't want to ignore unknown fields.
099         * @param ignoreNestedProperties the flag to set (default false)
100         */
101        public void setIgnoreNestedProperties(boolean ignoreNestedProperties) {
102                this.ignoreNestedProperties = ignoreNestedProperties;
103        }
104
105        /**
106         * Set name aliases.
107         * @param aliases a map of property name to aliases
108         */
109        public void setNameAliases(Map<String, List<String>> aliases) {
110                this.nameAliases = new LinkedMultiValueMap<String, String>(aliases);
111        }
112
113        /**
114         * Add aliases to the {@link DataBinder}.
115         * @param name the property name to alias
116         * @param alias aliases for the property names
117         * @return this instance
118         */
119        public RelaxedDataBinder withAlias(String name, String... alias) {
120                for (String value : alias) {
121                        this.nameAliases.add(name, value);
122                }
123                return this;
124        }
125
126        @Override
127        protected void doBind(MutablePropertyValues propertyValues) {
128                super.doBind(modifyProperties(propertyValues, getTarget()));
129        }
130
131        /**
132         * Modify the property values so that period separated property paths are valid for
133         * map keys. Also creates new maps for properties of map type that are null (assuming
134         * all maps are potentially nested). The standard bracket {@code[...]} dereferencing
135         * is also accepted.
136         * @param propertyValues the property values
137         * @param target the target object
138         * @return modified property values
139         */
140        private MutablePropertyValues modifyProperties(MutablePropertyValues propertyValues,
141                        Object target) {
142                propertyValues = getPropertyValuesForNamePrefix(propertyValues);
143                if (target instanceof MapHolder) {
144                        propertyValues = addMapPrefix(propertyValues);
145                }
146                BeanWrapper wrapper = new BeanWrapperImpl(target);
147                wrapper.setConversionService(
148                                new RelaxedConversionService(getConversionService()));
149                wrapper.setAutoGrowNestedPaths(true);
150                List<PropertyValue> sortedValues = new ArrayList<PropertyValue>();
151                Set<String> modifiedNames = new HashSet<String>();
152                List<String> sortedNames = getSortedPropertyNames(propertyValues);
153                for (String name : sortedNames) {
154                        PropertyValue propertyValue = propertyValues.getPropertyValue(name);
155                        PropertyValue modifiedProperty = modifyProperty(wrapper, propertyValue);
156                        if (modifiedNames.add(modifiedProperty.getName())) {
157                                sortedValues.add(modifiedProperty);
158                        }
159                }
160                return new MutablePropertyValues(sortedValues);
161        }
162
163        private List<String> getSortedPropertyNames(MutablePropertyValues propertyValues) {
164                List<String> names = new LinkedList<String>();
165                for (PropertyValue propertyValue : propertyValues.getPropertyValueList()) {
166                        names.add(propertyValue.getName());
167                }
168                sortPropertyNames(names);
169                return names;
170        }
171
172        /**
173         * Sort by name so that parent properties get processed first (e.g. 'foo.bar' before
174         * 'foo.bar.spam'). Don't use Collections.sort() because the order might be
175         * significant for other property names (it shouldn't be but who knows what people
176         * might be relying on, e.g. HSQL has a JDBCXADataSource where "databaseName" is a
177         * synonym for "url").
178         * @param names the names to sort
179         */
180        private void sortPropertyNames(List<String> names) {
181                for (String name : new ArrayList<String>(names)) {
182                        int propertyIndex = names.indexOf(name);
183                        BeanPath path = new BeanPath(name);
184                        for (String prefix : path.prefixes()) {
185                                int prefixIndex = names.indexOf(prefix);
186                                if (prefixIndex >= propertyIndex) {
187                                        // The child property has a parent in the list in the wrong order
188                                        names.remove(name);
189                                        names.add(prefixIndex, name);
190                                }
191                        }
192                }
193        }
194
195        private MutablePropertyValues addMapPrefix(MutablePropertyValues propertyValues) {
196                MutablePropertyValues rtn = new MutablePropertyValues();
197                for (PropertyValue pv : propertyValues.getPropertyValues()) {
198                        rtn.add("map." + pv.getName(), pv.getValue());
199                }
200                return rtn;
201        }
202
203        private MutablePropertyValues getPropertyValuesForNamePrefix(
204                        MutablePropertyValues propertyValues) {
205                if (!StringUtils.hasText(this.namePrefix) && !this.ignoreNestedProperties) {
206                        return propertyValues;
207                }
208                MutablePropertyValues rtn = new MutablePropertyValues();
209                for (PropertyValue value : propertyValues.getPropertyValues()) {
210                        String name = value.getName();
211                        for (String prefix : new RelaxedNames(stripLastDot(this.namePrefix))) {
212                                for (String separator : new String[] { ".", "_" }) {
213                                        String candidate = (StringUtils.hasLength(prefix) ? prefix + separator
214                                                        : prefix);
215                                        if (name.startsWith(candidate)) {
216                                                name = name.substring(candidate.length());
217                                                if (!(this.ignoreNestedProperties && name.contains("."))) {
218                                                        PropertyOrigin propertyOrigin = OriginCapablePropertyValue
219                                                                        .getOrigin(value);
220                                                        rtn.addPropertyValue(new OriginCapablePropertyValue(name,
221                                                                        value.getValue(), propertyOrigin));
222                                                }
223                                        }
224                                }
225                        }
226                }
227                return rtn;
228        }
229
230        private String stripLastDot(String string) {
231                if (StringUtils.hasLength(string) && string.endsWith(".")) {
232                        string = string.substring(0, string.length() - 1);
233                }
234                return string;
235        }
236
237        private PropertyValue modifyProperty(BeanWrapper target,
238                        PropertyValue propertyValue) {
239                String name = propertyValue.getName();
240                String normalizedName = normalizePath(target, name);
241                if (!normalizedName.equals(name)) {
242                        return new PropertyValue(normalizedName, propertyValue.getValue());
243                }
244                return propertyValue;
245        }
246
247        /**
248         * Normalize a bean property path to a format understood by a BeanWrapper. This is
249         * used so that
250         * <ul>
251         * <li>Fuzzy matching can be employed for bean property names</li>
252         * <li>Period separators can be used instead of indexing ([...]) for map keys</li>
253         * </ul>
254         * @param wrapper a bean wrapper for the object to bind
255         * @param path the bean path to bind
256         * @return a transformed path with correct bean wrapper syntax
257         */
258        protected String normalizePath(BeanWrapper wrapper, String path) {
259                return initializePath(wrapper, new BeanPath(path), 0);
260        }
261
262        @Override
263        protected AbstractPropertyBindingResult createBeanPropertyBindingResult() {
264                return new RelaxedBeanPropertyBindingResult(getTarget(), getObjectName(),
265                                isAutoGrowNestedPaths(), getAutoGrowCollectionLimit(),
266                                getConversionService());
267        }
268
269        private String initializePath(BeanWrapper wrapper, BeanPath path, int index) {
270                String prefix = path.prefix(index);
271                String key = path.name(index);
272                if (path.isProperty(index)) {
273                        key = getActualPropertyName(wrapper, prefix, key);
274                        path.rename(index, key);
275                }
276                if (path.name(++index) == null) {
277                        return path.toString();
278                }
279                String name = path.prefix(index);
280                TypeDescriptor descriptor = wrapper.getPropertyTypeDescriptor(name);
281                if (descriptor == null || descriptor.isMap()) {
282                        if (isMapValueStringType(descriptor)
283                                        || isBlanked(wrapper, name, path.name(index))) {
284                                path.collapseKeys(index);
285                        }
286                        path.mapIndex(index);
287                        extendMapIfNecessary(wrapper, path, index);
288                }
289                else if (descriptor.isCollection()) {
290                        extendCollectionIfNecessary(wrapper, path, index);
291                }
292                else if (descriptor.getType().equals(Object.class)) {
293                        if (isBlanked(wrapper, name, path.name(index))) {
294                                path.collapseKeys(index);
295                        }
296                        path.mapIndex(index);
297                        if (path.isLastNode(index)) {
298                                wrapper.setPropertyValue(path.toString(), BLANK);
299                        }
300                        else {
301                                String next = path.prefix(index + 1);
302                                if (wrapper.getPropertyValue(next) == null) {
303                                        wrapper.setPropertyValue(next, new LinkedHashMap<String, Object>());
304                                }
305                        }
306                }
307                return initializePath(wrapper, path, index);
308        }
309
310        private boolean isMapValueStringType(TypeDescriptor descriptor) {
311                if (descriptor == null || descriptor.getMapValueTypeDescriptor() == null) {
312                        return false;
313                }
314                if (Properties.class.isAssignableFrom(descriptor.getObjectType())) {
315                        // Properties is declared as Map<Object,Object> but we know it's really
316                        // Map<String,String>
317                        return true;
318                }
319                Class<?> valueType = descriptor.getMapValueTypeDescriptor().getObjectType();
320                return (valueType != null && CharSequence.class.isAssignableFrom(valueType));
321        }
322
323        @SuppressWarnings("rawtypes")
324        private boolean isBlanked(BeanWrapper wrapper, String propertyName, String key) {
325                Object value = (wrapper.isReadableProperty(propertyName)
326                                ? wrapper.getPropertyValue(propertyName) : null);
327                if (value instanceof Map) {
328                        if (((Map) value).get(key) == BLANK) {
329                                return true;
330                        }
331                }
332                return false;
333        }
334
335        private void extendCollectionIfNecessary(BeanWrapper wrapper, BeanPath path,
336                        int index) {
337                String name = path.prefix(index);
338                TypeDescriptor elementDescriptor = wrapper.getPropertyTypeDescriptor(name)
339                                .getElementTypeDescriptor();
340                if (!elementDescriptor.isMap() && !elementDescriptor.isCollection()
341                                && !elementDescriptor.getType().equals(Object.class)) {
342                        return;
343                }
344                Object extend = new LinkedHashMap<String, Object>();
345                if (!elementDescriptor.isMap() && path.isArrayIndex(index)) {
346                        extend = new ArrayList<Object>();
347                }
348                wrapper.setPropertyValue(path.prefix(index + 1), extend);
349        }
350
351        private void extendMapIfNecessary(BeanWrapper wrapper, BeanPath path, int index) {
352                String name = path.prefix(index);
353                TypeDescriptor parent = wrapper.getPropertyTypeDescriptor(name);
354                if (parent == null) {
355                        return;
356                }
357                TypeDescriptor descriptor = parent.getMapValueTypeDescriptor();
358                if (descriptor == null) {
359                        descriptor = TypeDescriptor.valueOf(Object.class);
360                }
361                if (!descriptor.isMap() && !descriptor.isCollection()
362                                && !descriptor.getType().equals(Object.class)) {
363                        return;
364                }
365                String extensionName = path.prefix(index + 1);
366                if (wrapper.isReadableProperty(extensionName)) {
367                        Object currentValue = wrapper.getPropertyValue(extensionName);
368                        if ((descriptor.isCollection() && currentValue instanceof Collection)
369                                        || (!descriptor.isCollection() && currentValue instanceof Map)) {
370                                return;
371                        }
372                }
373                Object extend = new LinkedHashMap<String, Object>();
374                if (descriptor.isCollection()) {
375                        extend = new ArrayList<Object>();
376                }
377                if (descriptor.getType().equals(Object.class) && path.isLastNode(index)) {
378                        extend = BLANK;
379                }
380                wrapper.setPropertyValue(extensionName, extend);
381        }
382
383        private String getActualPropertyName(BeanWrapper target, String prefix, String name) {
384                String propertyName = resolvePropertyName(target, prefix, name);
385                if (propertyName == null) {
386                        propertyName = resolveNestedPropertyName(target, prefix, name);
387                }
388                return (propertyName == null ? name : propertyName);
389        }
390
391        private String resolveNestedPropertyName(BeanWrapper target, String prefix,
392                        String name) {
393                StringBuilder candidate = new StringBuilder();
394                for (String field : name.split("[_\\-\\.]")) {
395                        candidate.append(candidate.length() > 0 ? "." : "");
396                        candidate.append(field);
397                        String nested = resolvePropertyName(target, prefix, candidate.toString());
398                        if (nested != null) {
399                                Class<?> type = target.getPropertyType(nested);
400                                if ((type != null) && Map.class.isAssignableFrom(type)) {
401                                        // Special case for map property (gh-3836).
402                                        return nested + "[" + name.substring(candidate.length() + 1) + "]";
403                                }
404                                String propertyName = resolvePropertyName(target,
405                                                joinString(prefix, nested),
406                                                name.substring(candidate.length() + 1));
407                                if (propertyName != null) {
408                                        return joinString(nested, propertyName);
409                                }
410                        }
411                }
412                return null;
413        }
414
415        private String resolvePropertyName(BeanWrapper target, String prefix, String name) {
416                Iterable<String> names = getNameAndAliases(name);
417                for (String nameOrAlias : names) {
418                        for (String candidate : new RelaxedNames(nameOrAlias)) {
419                                try {
420                                        if (target.getPropertyType(joinString(prefix, candidate)) != null) {
421                                                return candidate;
422                                        }
423                                }
424                                catch (InvalidPropertyException ex) {
425                                        // swallow and continue
426                                }
427                        }
428                }
429                return null;
430        }
431
432        private String joinString(String prefix, String name) {
433                return (StringUtils.hasLength(prefix) ? prefix + "." + name : name);
434        }
435
436        private Iterable<String> getNameAndAliases(String name) {
437                List<String> aliases = this.nameAliases.get(name);
438                if (aliases == null) {
439                        return Collections.singleton(name);
440                }
441                List<String> nameAndAliases = new ArrayList<String>(aliases.size() + 1);
442                nameAndAliases.add(name);
443                nameAndAliases.addAll(aliases);
444                return nameAndAliases;
445        }
446
447        private static Object wrapTarget(Object target) {
448                if (target instanceof Map) {
449                        @SuppressWarnings("unchecked")
450                        Map<String, Object> map = (Map<String, Object>) target;
451                        target = new MapHolder(map);
452                }
453                return target;
454        }
455
456        /**
457         * Holder to allow Map targets to be bound.
458         */
459        static class MapHolder {
460
461                private Map<String, Object> map;
462
463                MapHolder(Map<String, Object> map) {
464                        this.map = map;
465                }
466
467                public void setMap(Map<String, Object> map) {
468                        this.map = map;
469                }
470
471                public Map<String, Object> getMap() {
472                        return this.map;
473                }
474
475        }
476
477        /**
478         * A path though properties of a bean.
479         */
480        private static class BeanPath {
481
482                private List<PathNode> nodes;
483
484                BeanPath(String path) {
485                        this.nodes = splitPath(path);
486                }
487
488                public List<String> prefixes() {
489                        List<String> prefixes = new ArrayList<String>();
490                        for (int index = 1; index < this.nodes.size(); index++) {
491                                prefixes.add(prefix(index));
492                        }
493                        return prefixes;
494                }
495
496                public boolean isLastNode(int index) {
497                        return index >= this.nodes.size() - 1;
498                }
499
500                private List<PathNode> splitPath(String path) {
501                        List<PathNode> nodes = new ArrayList<PathNode>();
502                        String current = extractIndexedPaths(path, nodes);
503                        for (String name : StringUtils.delimitedListToStringArray(current, ".")) {
504                                if (StringUtils.hasText(name)) {
505                                        nodes.add(new PropertyNode(name));
506                                }
507                        }
508                        return nodes;
509                }
510
511                private String extractIndexedPaths(String path, List<PathNode> nodes) {
512                        int startRef = path.indexOf("[");
513                        String current = path;
514                        while (startRef >= 0) {
515                                if (startRef > 0) {
516                                        nodes.addAll(splitPath(current.substring(0, startRef)));
517                                }
518                                int endRef = current.indexOf("]", startRef);
519                                if (endRef > 0) {
520                                        String sub = current.substring(startRef + 1, endRef);
521                                        if (sub.matches("[0-9]+")) {
522                                                nodes.add(new ArrayIndexNode(sub));
523                                        }
524                                        else {
525                                                nodes.add(new MapIndexNode(sub));
526                                        }
527                                }
528                                current = current.substring(endRef + 1);
529                                startRef = current.indexOf("[");
530                        }
531                        return current;
532                }
533
534                public void collapseKeys(int index) {
535                        List<PathNode> revised = new ArrayList<PathNode>();
536                        for (int i = 0; i < index; i++) {
537                                revised.add(this.nodes.get(i));
538                        }
539                        StringBuilder builder = new StringBuilder();
540                        for (int i = index; i < this.nodes.size(); i++) {
541                                if (i > index) {
542                                        builder.append(".");
543                                }
544                                builder.append(this.nodes.get(i).name);
545                        }
546                        revised.add(new PropertyNode(builder.toString()));
547                        this.nodes = revised;
548                }
549
550                public void mapIndex(int index) {
551                        PathNode node = this.nodes.get(index);
552                        if (node instanceof PropertyNode) {
553                                node = ((PropertyNode) node).mapIndex();
554                        }
555                        this.nodes.set(index, node);
556                }
557
558                public String prefix(int index) {
559                        return range(0, index);
560                }
561
562                public void rename(int index, String name) {
563                        this.nodes.get(index).name = name;
564                }
565
566                public String name(int index) {
567                        if (index < this.nodes.size()) {
568                                return this.nodes.get(index).name;
569                        }
570                        return null;
571                }
572
573                private String range(int start, int end) {
574                        StringBuilder builder = new StringBuilder();
575                        for (int i = start; i < end; i++) {
576                                PathNode node = this.nodes.get(i);
577                                builder.append(node);
578                        }
579                        if (builder.toString().startsWith(("."))) {
580                                builder.replace(0, 1, "");
581                        }
582                        return builder.toString();
583                }
584
585                public boolean isArrayIndex(int index) {
586                        return this.nodes.get(index) instanceof ArrayIndexNode;
587                }
588
589                public boolean isProperty(int index) {
590                        return this.nodes.get(index) instanceof PropertyNode;
591                }
592
593                @Override
594                public String toString() {
595                        return prefix(this.nodes.size());
596                }
597
598                private static class PathNode {
599
600                        protected String name;
601
602                        PathNode(String name) {
603                                this.name = name;
604                        }
605
606                }
607
608                private static class ArrayIndexNode extends PathNode {
609
610                        ArrayIndexNode(String name) {
611                                super(name);
612                        }
613
614                        @Override
615                        public String toString() {
616                                return "[" + this.name + "]";
617                        }
618
619                }
620
621                private static class MapIndexNode extends PathNode {
622
623                        MapIndexNode(String name) {
624                                super(name);
625                        }
626
627                        @Override
628                        public String toString() {
629                                return "[" + this.name + "]";
630                        }
631
632                }
633
634                private static class PropertyNode extends PathNode {
635
636                        PropertyNode(String name) {
637                                super(name);
638                        }
639
640                        public MapIndexNode mapIndex() {
641                                return new MapIndexNode(this.name);
642                        }
643
644                        @Override
645                        public String toString() {
646                                return "." + this.name;
647                        }
648
649                }
650
651        }
652
653        /**
654         * Extended version of {@link BeanPropertyBindingResult} to support relaxed binding.
655         */
656        private static class RelaxedBeanPropertyBindingResult
657                        extends BeanPropertyBindingResult {
658
659                private RelaxedConversionService conversionService;
660
661                RelaxedBeanPropertyBindingResult(Object target, String objectName,
662                                boolean autoGrowNestedPaths, int autoGrowCollectionLimit,
663                                ConversionService conversionService) {
664                        super(target, objectName, autoGrowNestedPaths, autoGrowCollectionLimit);
665                        this.conversionService = new RelaxedConversionService(conversionService);
666                }
667
668                @Override
669                protected BeanWrapper createBeanWrapper() {
670                        BeanWrapper beanWrapper = new RelaxedBeanWrapper(getTarget());
671                        beanWrapper.setConversionService(this.conversionService);
672                        beanWrapper.registerCustomEditor(InetAddress.class, new InetAddressEditor());
673                        return beanWrapper;
674                }
675
676        }
677
678        /**
679         * Extended version of {@link BeanWrapperImpl} to support relaxed binding.
680         */
681        private static class RelaxedBeanWrapper extends BeanWrapperImpl {
682
683                private static final Set<String> BENIGN_PROPERTY_SOURCE_NAMES;
684
685                static {
686                        Set<String> names = new HashSet<String>();
687                        names.add(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME);
688                        names.add(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME);
689                        BENIGN_PROPERTY_SOURCE_NAMES = Collections.unmodifiableSet(names);
690                }
691
692                RelaxedBeanWrapper(Object target) {
693                        super(target);
694                }
695
696                @Override
697                public void setPropertyValue(PropertyValue pv) throws BeansException {
698                        try {
699                                super.setPropertyValue(pv);
700                        }
701                        catch (NotWritablePropertyException ex) {
702                                PropertyOrigin origin = OriginCapablePropertyValue.getOrigin(pv);
703                                if (isBenign(origin)) {
704                                        logger.debug("Ignoring benign property binding failure", ex);
705                                        return;
706                                }
707                                if (origin == null) {
708                                        throw ex;
709                                }
710                                throw new RelaxedBindingNotWritablePropertyException(ex, origin);
711                        }
712                }
713
714                private boolean isBenign(PropertyOrigin origin) {
715                        String name = (origin == null ? null : origin.getSource().getName());
716                        return BENIGN_PROPERTY_SOURCE_NAMES.contains(name);
717                }
718
719        }
720
721}