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}