package mil.nga.giat.geowave.core.cli.prefix; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameterized; import javassist.CannotCompileException; import javassist.ClassClassPath; import javassist.ClassPool; import javassist.CtClass; import javassist.CtField; import javassist.CtMethod; import javassist.NotFoundException; import javassist.bytecode.AccessFlag; import javassist.bytecode.AnnotationsAttribute; import javassist.bytecode.annotation.Annotation; import javassist.bytecode.annotation.ArrayMemberValue; import javassist.bytecode.annotation.BooleanMemberValue; import javassist.bytecode.annotation.MemberValue; import javassist.bytecode.annotation.StringMemberValue; /** * The translation map allows us to easily copy values from the facade objects * back to the original objects. */ public class JCommanderTranslationMap { private static Logger LOGGER = LoggerFactory.getLogger(JCommanderTranslationMap.class); // This package is where classes generated by this translator live in the // classpath. public static final String NAMES_MEMBER = "names"; public static final String REQUIRED_MEMBER = "required"; // HP Fortify "Hardcoded Password" false positive // This is a password label, not a password public static final String PASSWORD_MEMBER = "password"; public static final String PREFIX_SEPARATOR = "."; // Tells us how to translate a field (indexed by facade field id) to // the original objects and back. private final Map<String, TranslationEntry> translations = new LinkedHashMap<String, TranslationEntry>(); // These are the objects generated by createFacadeObjects() private List<Object> translatedObjects = null; public JCommanderTranslationMap() { } /** * Objects are the facades. * * @return */ public Collection<Object> getObjects() { return Collections.unmodifiableCollection(translatedObjects); } /** * Return all the translations. They are indexed by 'field name', where * field name is the field in the facade object. Allow the user to modify * them up until they create the facade objects * * @return */ public Map<String, TranslationEntry> getEntries() { if (translatedObjects != null) { return Collections.unmodifiableMap(translations); } return translations; } /** * Transfer the values from the facade objects to the original objects using * the translation map. */ public void transformToOriginal() { for (Object obj : translatedObjects) { for (Field field : obj.getClass().getDeclaredFields()) { TranslationEntry tEntry = translations.get(field.getName()); try { tEntry.getParam().set( tEntry.getObject(), field.get(obj)); } catch (IllegalArgumentException | IllegalAccessException e) { // Allow these, since they really shouldn't ever happen. LOGGER.warn( "Unable to return field object", e); } } } } /** * Transfer the values from the original objects to the facade objects using * the translation map. */ public void transformToFacade() { for (Object obj : translatedObjects) { for (Field field : obj.getClass().getDeclaredFields()) { TranslationEntry tEntry = translations.get(field.getName()); try { field.set( obj, tEntry.getParam().get( tEntry.getObject())); } catch (IllegalArgumentException | IllegalAccessException e) { // Ignore, no getter (if it's a method) or there was // a security violation. LOGGER.warn( "Unable to set field", e); } } } } /** * This is a mapping between the created facade's field (e.g., field_0) and * the JCommander parameter (param) which lives in the object it was parsed * from, 'item'. * * @param newFieldName * @param item * @param param * @param names * - the arguments values for this item. */ protected void addEntry( String newFieldName, Object item, Parameterized param, String prefix, AnnotatedElement member ) { translations.put( newFieldName, new TranslationEntry( param, item, prefix, member)); } /** * This will create the facade objects needed in order to parse the fields * represented in the translation map. * * @param map * @return */ public void createFacadeObjects() { if (translatedObjects != null) { throw new RuntimeException( "Cannot use the same translation " + "map twice"); } // Clear old objects. translatedObjects = new ArrayList<>(); // So we don't re-create classes we already created. Map<Class<?>, CtClass> createdClasses = new HashMap<Class<?>, CtClass>(); try { // This class pool will be used to find existing classes and create // new // classes. ClassPool classPool = ClassPool.getDefault(); ClassClassPath path = new ClassClassPath( JCommanderPrefixTranslator.class); classPool.insertClassPath(path); // Iterate the final translations and create the classes. for (Map.Entry<String, TranslationEntry> mapEntry : translations.entrySet()) { // Cache for later. String newFieldName = mapEntry.getKey(); TranslationEntry entry = mapEntry.getValue(); // This is the class we're making a facade of. Class<?> objectClass = entry.getObject().getClass(); // Get a CtClass reference to the item's class CtClass oldClass = classPool.get(objectClass.getName()); // Retrieve previously created class to add new field CtClass newClass = createdClasses.get(objectClass); // Create the class if we haven't yet. if (newClass == null) { // Create the class, so we can start adding the new facade // fields to it. newClass = JavassistUtils.generateEmptyClass(); // Copy over the @Parameters annotation, if it is set. JavassistUtils.copyClassAnnotations( oldClass, newClass); // Store for later. createdClasses.put( objectClass, newClass); } // This is a field or method, which means we should add it to // our current // object. CtField newField = null; if (!entry.isMethod()) { // This is a field. This is easy! Just clone the field. It // will // copy over the annotations as well. newField = new CtField( oldClass.getField(entry.getParam().getName()), newClass); } else { // This is a method. This is hard. We can create a field // with the same name, but we gotta copy over the // annotations manually. // We also don't want to copy annotations that specifically // target // METHOD, so we'll only clone annotations that can target // FIELD. CtClass fieldType = classPool.get(entry.getParam().getType().getName()); newField = new CtField( fieldType, entry.getParam().getName(), newClass); // We need to find the existing method CtMethod reference, // so we can clone // annotations. This method is ugly. Do not look at it. CtMethod method = JavassistUtils.findMethod( oldClass, (Method) entry.getMember()); // Copy the annotations! JavassistUtils.copyMethodAnnotationsToField( method, newField); } // This is where the meat of the prefix algorithm is. If we have // a prefix // for this class(in ParseContext), then we apply it to the // attributes by // iterating over the annotations, looking for a 'names' member // variable, and // overriding the values one by one. if (entry.getPrefix().length() > 0) { overrideParameterPrefixes( newField, entry.getPrefixedNames()); } // This is a fix for #95 ( // https://github.com/cbeust/jcommander/issues/95 ). // I need this for cpstore, cpindex, etc, but it's only been // implemented as of 1.55, // an unreleased version. if (entry.isRequired() && entry.hasValue()) { disableBooleanMember( REQUIRED_MEMBER, newField); } if (entry.isPassword() && entry.hasValue()) { disableBooleanMember( PASSWORD_MEMBER, newField); } // Rename the field so there are no conflicts. Name really // doesn't matter, // but it's used for translation in transMap. newField.setName(newFieldName); newField.getFieldInfo().setAccessFlags( AccessFlag.PUBLIC); // Add the field to the class newClass.addField(newField); } // Iterate TranslationEntry // Convert the translated CtClass to an actual class. for (CtClass clz : createdClasses.values()) { Class<?> toClass = clz.toClass(); Object instance = toClass.newInstance(); translatedObjects.add(instance); } } catch (InstantiationException | IllegalAccessException | NotFoundException | IllegalStateException | NullPointerException | CannotCompileException e) { LOGGER.error( "Unable to create classes", e); throw new RuntimeException(); } /* * catch (Exception e) { // This should never happen, but if it does, * then it's a programmer // error. throw new RuntimeException( e); } */ } /** * Iterate the annotations, look for a 'names' parameter, and override it to * prepend the given prefix. * * @param field * @param prefix */ private void overrideParameterPrefixes( CtField field, String[] names ) { // This is the JCommander package name String packageName = JCommander.class.getPackage().getName(); AnnotationsAttribute fieldAttributes = (AnnotationsAttribute) field.getFieldInfo().getAttribute( AnnotationsAttribute.visibleTag); // Look for annotations that have a 'names' attribute, and whose package // starts with the expected JCommander package. for (Annotation annotation : fieldAttributes.getAnnotations()) { if (annotation.getTypeName().startsWith( packageName)) { // See if it has a 'names' member variable. MemberValue namesMember = annotation.getMemberValue(NAMES_MEMBER); // We have a names member!!! if (namesMember != null) { ArrayMemberValue arrayNamesMember = (ArrayMemberValue) namesMember; // Iterate and transform each item in 'names()' list and // transform it. MemberValue[] newMemberValues = new MemberValue[names.length]; for (int i = 0; i < names.length; i++) { newMemberValues[i] = new StringMemberValue( names[i], field.getFieldInfo2().getConstPool()); } // Override the member values in nameMember with the new // one's we've generated arrayNamesMember.setValue(newMemberValues); // This is KEY! For some reason, the existing annotation // will not be modified unless // you call 'setAnnotation' here. I'm guessing // 'getAnnotation()' creates a copy. fieldAttributes.setAnnotation(annotation); // Finished processing names. break; } } } } /** * Iterate the annotations, look for a 'required' parameter, and set it to * false. * * @param field * @param prefix */ private void disableBooleanMember( String booleanMemberName, CtField field ) { // This is the JCommander package name String packageName = JCommander.class.getPackage().getName(); AnnotationsAttribute fieldAttributes = (AnnotationsAttribute) field.getFieldInfo().getAttribute( AnnotationsAttribute.visibleTag); // Look for annotations that have a 'names' attribute, and whose package // starts with the expected JCommander package. for (Annotation annotation : fieldAttributes.getAnnotations()) { if (annotation.getTypeName().startsWith( packageName)) { // See if it has a 'names' member variable. MemberValue requiredMember = annotation.getMemberValue(booleanMemberName); // We have a names member!!! if (requiredMember != null) { BooleanMemberValue booleanRequiredMember = (BooleanMemberValue) requiredMember; // Set it to not required. booleanRequiredMember.setValue(false); // This is KEY! For some reason, the existing annotation // will not be modified unless // you call 'setAnnotation' here. I'm guessing // 'getAnnotation()' creates a copy. fieldAttributes.setAnnotation(annotation); // Finished processing names. break; } } } } }