001package com.randomnoun.maven.plugin.yamlCombine;
002
003// from https://stackoverflow.com/questions/31534014/keep-tags-order-using-snakeyaml
004
005import java.beans.FeatureDescriptor;
006import java.beans.IntrospectionException;
007import java.beans.Introspector;
008import java.beans.PropertyDescriptor;
009import java.lang.reflect.Field;
010import java.lang.reflect.Method;
011import java.lang.reflect.Modifier;
012import java.util.*;
013
014import org.yaml.snakeyaml.error.YAMLException;
015import org.yaml.snakeyaml.introspector.*;
016import org.yaml.snakeyaml.util.PlatformFeatureDetector;
017
018/** A class to retain ordering in YAML files */
019public class CustomPropertyUtils extends PropertyUtils {
020
021    private final Map<Class<?>, Map<String, Property>> propertiesCache = new HashMap<Class<?>, Map<String, Property>>();
022    private final Map<Class<?>, Set<Property>> readableProperties = new HashMap<Class<?>, Set<Property>>();
023    private BeanAccess beanAccess = BeanAccess.DEFAULT;
024    private boolean allowReadOnlyProperties = false;
025    private boolean skipMissingProperties = false;
026
027    private PlatformFeatureDetector platformFeatureDetector;
028
029    public CustomPropertyUtils() {
030        this(new PlatformFeatureDetector());
031    }
032
033    CustomPropertyUtils(PlatformFeatureDetector platformFeatureDetector) {
034        this.platformFeatureDetector = platformFeatureDetector;
035
036        /*
037         * Android lacks much of java.beans (including the Introspector class, used here), because java.beans classes tend to rely on java.awt, which isn't
038         * supported in the Android SDK. That means we have to fall back on FIELD access only when SnakeYAML is running on the Android Runtime.
039         */
040        if (platformFeatureDetector.isRunningOnAndroid()) {
041            beanAccess = BeanAccess.FIELD;
042        }
043    }
044
045    protected Map<String, Property> getPropertiesMap(Class<?> type, BeanAccess bAccess) {
046        if (propertiesCache.containsKey(type)) {
047            return propertiesCache.get(type);
048        }
049
050        Map<String, Property> properties = new LinkedHashMap<String, Property>();
051        boolean inaccessableFieldsExist = false;
052        switch (bAccess) {
053            case FIELD:
054                for (Class<?> c = type; c != null; c = c.getSuperclass()) {
055                    for (Field field : c.getDeclaredFields()) {
056                        int modifiers = field.getModifiers();
057                        if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers)
058                                && !properties.containsKey(field.getName())) {
059                            properties.put(field.getName(), new FieldProperty(field));
060                        }
061                    }
062                }
063                break;
064            default:
065                // add JavaBean properties
066                try {
067                    for (PropertyDescriptor property : Introspector.getBeanInfo(type)
068                            .getPropertyDescriptors()) {
069                        Method readMethod = property.getReadMethod();
070                        if ((readMethod == null || !readMethod.getName().equals("getClass"))
071                                && !isTransient(property)) {
072                            properties.put(property.getName(), new MethodProperty(property));
073                        }
074                    }
075                } catch (IntrospectionException e) {
076                    throw new YAMLException(e);
077                }
078
079                // add public fields
080                for (Class<?> c = type; c != null; c = c.getSuperclass()) {
081                    for (Field field : c.getDeclaredFields()) {
082                        int modifiers = field.getModifiers();
083                        if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers)) {
084                            if (Modifier.isPublic(modifiers)) {
085                                properties.put(field.getName(), new FieldProperty(field));
086                            } else {
087                                inaccessableFieldsExist = true;
088                            }
089                        }
090                    }
091                }
092                break;
093        }
094        if (properties.isEmpty() && inaccessableFieldsExist) {
095            throw new YAMLException("No JavaBean properties found in " + type.getName());
096        }
097        System.out.println(properties);
098        propertiesCache.put(type, properties);
099        return properties;
100    }
101
102    private static final String TRANSIENT = "transient";
103
104    private boolean isTransient(FeatureDescriptor fd) {
105        return Boolean.TRUE.equals(fd.getValue(TRANSIENT));
106    }
107
108    public Set<Property> getProperties(Class<? extends Object> type) {
109        return getProperties(type, beanAccess);
110    }
111
112    public Set<Property> getProperties(Class<? extends Object> type, BeanAccess bAccess) {
113        if (readableProperties.containsKey(type)) {
114            return readableProperties.get(type);
115        }
116        Set<Property> properties = createPropertySet(type, bAccess);
117        readableProperties.put(type, properties);
118        return properties;
119    }
120
121    protected Set<Property> createPropertySet(Class<? extends Object> type, BeanAccess bAccess) {
122        Set<Property> properties = new LinkedHashSet<>();
123        Collection<Property> props = getPropertiesMap(type, bAccess).values();
124        for (Property property : props) {
125            if (property.isReadable() && (allowReadOnlyProperties || property.isWritable())) {
126                properties.add(property);
127            }
128        }
129        return properties;
130    }
131
132    public Property getProperty(Class<? extends Object> type, String name) {
133        return getProperty(type, name, beanAccess);
134    }
135
136    public Property getProperty(Class<? extends Object> type, String name, BeanAccess bAccess) {
137        Map<String, Property> properties = getPropertiesMap(type, bAccess);
138        Property property = properties.get(name);
139        if (property == null && skipMissingProperties) {
140            property = new MissingProperty(name);
141        }
142        if (property == null) {
143            throw new YAMLException(
144                    "Unable to find property '" + name + "' on class: " + type.getName());
145        }
146        return property;
147    }
148
149    public void setBeanAccess(BeanAccess beanAccess) {
150        if (platformFeatureDetector.isRunningOnAndroid() && beanAccess != BeanAccess.FIELD) {
151            throw new IllegalArgumentException(
152                    "JVM is Android - only BeanAccess.FIELD is available");
153        }
154
155        if (this.beanAccess != beanAccess) {
156            this.beanAccess = beanAccess;
157            propertiesCache.clear();
158            readableProperties.clear();
159        }
160    }
161
162    public void setAllowReadOnlyProperties(boolean allowReadOnlyProperties) {
163        if (this.allowReadOnlyProperties != allowReadOnlyProperties) {
164            this.allowReadOnlyProperties = allowReadOnlyProperties;
165            readableProperties.clear();
166        }
167    }
168
169    public boolean isAllowReadOnlyProperties() {
170        return allowReadOnlyProperties;
171    }
172
173    /**
174     * Skip properties that are missing during deserialization of YAML to a Java
175     * object. The default is false.
176     *
177     * @param skipMissingProperties
178     *            true if missing properties should be skipped, false otherwise.
179     */
180    public void setSkipMissingProperties(boolean skipMissingProperties) {
181        if (this.skipMissingProperties != skipMissingProperties) {
182            this.skipMissingProperties = skipMissingProperties;
183            readableProperties.clear();
184        }
185    }
186
187    public boolean isSkipMissingProperties() {
188        return skipMissingProperties;
189    }
190}