Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.fujitsu.vdmj.mapper.ClassMapper Maven / Gradle / Ivy
/*******************************************************************************
*
* Copyright (c) 2016 Fujitsu Services Ltd.
*
* Author: Nick Battle
*
* This file is part of VDMJ.
*
* VDMJ is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VDMJ is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VDMJ. If not, see .
* SPDX-License-Identifier: GPL-3.0-or-later
*
******************************************************************************/
package com.fujitsu.vdmj.mapper;
import java.io.File;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.Vector;
import com.fujitsu.vdmj.config.Properties;
import com.fujitsu.vdmj.util.GetResource;
/**
* A class to map classes and extend trees of objects.
*/
public class ClassMapper
{
/** The mappers that have already been loaded, indexed by resource name */
private final static Map mappers = new HashMap();
/** Resource name for mapping search extensions */
private static final String MAPPINGS = "vdmj.mappings";
/**
* These caches hold the object references converted so far, and keep a stack of
* the objects that are currently being processed.
*/
private final Stack inProgress = new Stack();
private boolean dumpedProgress = false;
private final Map converted = new HashMap();
private long loadTimeMs;
/**
* Get an instance of a mapper, defined by the mapspec file name (resource).
* The error stream can be passed too, with a default getInstance that
* uses System.err.
*/
public static ClassMapper getInstance(String config, PrintStream err)
{
ClassMapper mapper = mappers.get(config);
if (mapper == null)
{
mapper = new ClassMapper(config, err);
mappers.put(config, mapper);
}
return mapper;
}
public static ClassMapper getInstance(String config)
{
return getInstance(config, System.err);
}
/**
* Initialise the progress stack and converted objects map.
*/
public ClassMapper init()
{
inProgress.clear();
converted.clear();
dumpedProgress = false;
for (Method m: initializers)
{
try
{
m.invoke(null, (Object[])null);
}
catch (Exception e)
{
throw new RuntimeException("Error in mapping initialzer", e);
}
}
return this; // Convenient for getInstance().init().convert(obj)
}
/**
* Fields used during the processing of the configuration file
*/
private final PrintStream errorStream;
private final String configFile;
private Field SELF;
private String srcPackage = "";
private String destPackage = "";
private int lineNo = 0;
private int errorCount = 0;
/**
* The private constructor, passed the resource name of the mapping file.
*/
private ClassMapper(String config, PrintStream err)
{
errorStream = err;
configFile = config;
long before = System.currentTimeMillis();
try
{
SELF = ClassMapper.class.getDeclaredField("SELF");
readMappings();
verifyConstructors();
}
catch (Exception e)
{
error(e.getMessage());
}
loadTimeMs = System.currentTimeMillis() - before;
if (errorCount > 0)
{
errorStream.println("Aborting with " + errorCount + " errors");
throw new ClassMapperException(config);
}
}
/** The set of class mappings for one loaded file */
private final Map, MapParams> mappings = new HashMap, MapParams>();
/** A list of methods to call in the init() processing */
private final List initializers = new Vector();
/** If the mark() method has been called, but not restore(), holds converted IDs */
private Map> marked = new HashMap>();
/**
* A class to define how to construct one destPackage class, passing srcPackage
* object fields to the Constructor and setter methods.
*/
private static class MapParams
{
public final int lineNo;
public final Class> srcClass;
public final Class> destClass;
public final List ctorFields;
public final List setterFields;
public final boolean unmapped;
public Constructor> constructor;
public Method[] setters;
public MapParams(int lineNo, Class> srcClass, Class> destClass,
List ctorFields, List setterFields, boolean unmapped)
{
this.lineNo = lineNo;
this.srcClass = srcClass;
this.destClass = destClass;
this.ctorFields = ctorFields;
this.setterFields = setterFields;
this.unmapped = unmapped;
}
@Override
public String toString()
{
if (unmapped)
{
return "unmapped " + srcClass.getName() + ";";
}
else
{
StringBuilder sb = new StringBuilder();
sb.append("map ");
sb.append(srcClass.getSimpleName());
sb.append("{");
String sep = "";
for (Field setter: ctorFields)
{
sb.append(sep);
sb.append(setter.getName());
sep = ", ";
}
sb.append("}");
sb.append(" to ");
sb.append(destClass.getSimpleName());
sb.append("(");
sep = "";
for (Field setter: ctorFields)
{
sb.append(sep);
sb.append(setter.getName());
sep = ", ";
}
sb.append(")");
if (!setterFields.isEmpty())
{
sb.append(" set ");
sep = "";
for (Field setter: setterFields)
{
sb.append(sep);
sb.append(setter.getName());
sep = ", ";
}
}
sb.append(";");
return sb.toString();
}
}
}
private void error(String message)
{
errorStream.println(configFile + " line " + lineNo + ": " + message);
errorCount++;
}
/**
* Read mappings file(s) from the classpath and populate the mappings table.
*/
private void readMappings() throws Exception
{
Enumeration urls = this.getClass().getClassLoader().getResources(configFile);
while (urls.hasMoreElements())
{
URL url = urls.nextElement();
readMapping(configFile, url.openStream());
}
/**
* You can add extra file locations by setting the vdmj.mapping.search_path property,
* or by putting resource folders in "vdmj.mappings". This allows more than one mapping
* file of the same name to be included within one jar file.
*/
List alternativePaths = new Vector();
String mappingPath = Properties.mapping_search_path;
if (mappingPath != null)
{
for (String classpath: mappingPath.split(File.pathSeparator))
{
alternativePaths.add(classpath + "/" + configFile); // NB. Use slash here!
}
}
else
{
for (String classpath: GetResource.readResource(MAPPINGS))
{
alternativePaths.add(classpath + "/" + configFile); // NB. Use slash here!
}
}
for (String resourceName: alternativePaths)
{
InputStream is = getClass().getResourceAsStream(resourceName);
if (is != null)
{
readMapping(resourceName, is);
}
}
}
private void readMapping(String filename, InputStream is) throws Exception
{
MappingReader reader = new MappingReader(filename, is);
try
{
boolean eof = false;
while (!eof)
{
Mapping command = reader.readCommand();
lineNo = command.lineNo;
switch (command.type)
{
case PACKAGE:
processPackage(command);
break;
case MAP:
processMap(command);
break;
case UNMAPPED:
processUnmapped(command);
break;
case INIT:
processInit(command);
break;
case EOF:
eof = true;
break;
case ERROR:
// try next line
errorCount++;
break;
}
}
}
finally
{
reader.close();
}
}
private void processInit(Mapping command)
{
String classname = null;
String methodname = null;
try
{
int end = command.source.lastIndexOf('.');
if (end > 0)
{
classname = command.source.substring(0, end);
methodname = command.source.substring(end + 1);
Class> initClass = Class.forName(classname);
Method initMethod = initClass.getMethod(methodname, (Class>[])null);
if (!Modifier.isStatic(initMethod.getModifiers()))
{
error("Init method is not static: " + classname + "." + methodname + "()");
}
else
{
initializers.add(initMethod);
}
}
else
{
error("Init entry malformed: " + command.source);
}
}
catch (ClassNotFoundException e)
{
error("No such class: " + classname);
}
catch (NoSuchMethodException e)
{
error("Cannot find method: " + classname + "." + methodname + "()");
}
}
private void processUnmapped(Mapping command)
{
try
{
Class> toIgnore = Class.forName(command.source);
if (mappings.containsKey(toIgnore))
{
errorStream.println("Class is already mapped: " + toIgnore.getName());
}
// else
{
mappings.put(toIgnore, new MapParams(lineNo, toIgnore, toIgnore, null, null, true));
}
}
catch (ClassNotFoundException e)
{
error("No such class: " + command.source);
}
}
private void processPackage(Mapping command)
{
srcPackage = command.source;
destPackage = command.destination;
}
private void processMap(Mapping command) throws Exception
{
String srcClassname = command.source;
List srcParams = command.varnames;
String destClassname = command.destination;
List destParams = command.paramnames;
List destSetters = command.setnames;
try
{
Class> srcClass = Class.forName(srcPackage + "." + srcClassname);
Class> destClass = Class.forName(destPackage + "." + destClassname);
Map srcFields = new HashMap();
for (String fieldname: srcParams)
{
srcFields.put(fieldname, findField(srcClass, fieldname));
}
if (Modifier.isAbstract(srcClass.getModifiers()))
{
if (!Modifier.isAbstract(destClass.getModifiers()))
{
error("Source " + srcClassname + " is abstract, but mapping is not");
}
if (!destParams.isEmpty())
{
error("Abstract class cannot have ctor parameter substitutions");
}
}
else if (Modifier.isAbstract(destClass.getModifiers()))
{
error("Mapped " + destClassname + " is abstract, but source is not");
}
List ctorFields = new Vector();
List setterFields = new Vector();
for (String field: destParams)
{
if (field.equals("this"))
{
ctorFields.add(SELF);
}
else if (srcFields.containsKey(field))
{
ctorFields.add(srcFields.get(field));
}
else
{
error("Field not identified in " + srcClassname + ": " + field);
}
}
for (String field: destSetters)
{
if (srcFields.containsKey(field))
{
setterFields.add(srcFields.get(field));
}
else
{
error("Field not identified in " + srcClassname + ": " + field);
}
}
for (String field: srcParams)
{
if (!destParams.contains(field) && !destSetters.contains(field))
{
error("Field not used in constructor or setters, " + destClassname + ": " + field);
}
}
for (String field: destParams)
{
if (destSetters.contains(field))
{
error("Field used in constructor and setter, " + destClassname + ": " + field);
}
}
if (mappings.containsKey(srcClass))
{
errorStream.println("Class is already mapped: " + srcClass.getName());
}
// else
{
mappings.put(srcClass, new MapParams(lineNo, srcClass, destClass, ctorFields, setterFields, false));
}
}
catch (ClassNotFoundException e)
{
error("No such class: " + e.getMessage());
}
catch (NoSuchFieldException e)
{
error("No such field in " + srcClassname + ": " + e.getMessage());
}
}
private Field findField(Class> src, String field) throws NoSuchFieldException, SecurityException
{
if (src.getSuperclass() != null)
{
try
{
return findField(src.getSuperclass(), field);
}
catch (NoSuchFieldException e)
{
// drop through
}
}
return src.getDeclaredField(field);
}
/**
* We cannot verify the constructors exist until all the mappings have been read,
* because we map the parameter types from the source as well. This method checks
* that all of the mapped constructors exist in the destination classes.
*/
private void verifyConstructors()
{
for (Class> entry: mappings.keySet())
{
MapParams mp = mappings.get(entry);
if (mp.unmapped)
{
continue; // fine, it's not mapped
}
if (!mp.srcClass.getPackage().getName().startsWith("com.fujitsu.vdmj.ast") &&
!Mappable.class.isAssignableFrom(mp.srcClass))
{
error("map source class is not Mappable: " + mp.srcClass.getName());
}
if (!Mappable.class.isAssignableFrom(mp.destClass))
{
error("map target class is not Mappable: " + mp.destClass.getName());
}
if (Modifier.isAbstract(entry.getModifiers()))
{
continue; // fine, it will never be instantiated anyway
}
else
{
Class>[] paramTypes = new Class>[mp.ctorFields.size()];
int a = 0;
for (Field field: mp.ctorFields)
{
Class> fieldType = null;
MapParams mapping = null;
if (field == SELF) // ie. "this"
{
fieldType = mp.srcClass;
mapping = null;
}
else
{
fieldType = field.getType();
mapping = mappings.get(fieldType);
}
if (mapping == null || mapping.unmapped)
{
paramTypes[a++] = fieldType; // eg. java.lang.String
}
else
{
paramTypes[a++] = mapping.destClass;
}
}
try
{
lineNo = mp.lineNo; // For error reporting :)
mp.constructor = mp.destClass.getConstructor(paramTypes);
}
catch (NoSuchMethodException e)
{
error("No such constructor: " + mp.destClass.getSimpleName() + "(" + typeString(paramTypes) + ")");
errorStream.println("Fields available from " + entry.getSimpleName() + ":");
for (Field f: getAllFields(entry))
{
errorStream.println(" " + f.getType().getSimpleName() + " " + f.getName());
}
errorStream.println("Constructors available for " + mp.destClass.getSimpleName() + ":");
for (Constructor> ctor: mp.destClass.getConstructors())
{
errorStream.println(" " + mp.destClass.getSimpleName() + "(" + typeString(ctor.getParameterTypes()) + ")");
}
errorStream.println();
}
mp.setters = new Method[mp.setterFields.size()];
a = 0;
for (Field field: mp.setterFields)
{
Class> fieldType = field.getType();
MapParams mapping = mappings.get(fieldType);
Class> argType = null;
if (mapping == null || mapping.unmapped)
{
argType = fieldType; // eg. java.lang.String unmapped
}
else
{
argType = mapping.destClass;
}
StringBuilder name = new StringBuilder(field.getName());
name.setCharAt(0, Character.toUpperCase(name.charAt(0)));
try
{
lineNo = mp.lineNo; // For error reporting :)
mp.setters[a++] = mp.destClass.getMethod("set" + name, argType);
}
catch (NoSuchMethodException e)
{
error("No such setter: " + mp.destClass.getSimpleName() +
".set" + name + "(" + argType.getSimpleName() + ")");
}
}
}
}
}
private String typeString(Class>[] paramTypes)
{
StringBuffer sb = new StringBuffer();
String prefix = "";
for (Class> type: paramTypes)
{
sb.append(prefix + type.getSimpleName());
prefix = ", ";
}
return sb.toString();
}
private Set getAllFields(Class> clazz)
{
Set allFields = new HashSet();
for (Field f: clazz.getFields())
{
allFields.add(f); // Including inherited fields
}
for (Field f: clazz.getDeclaredFields())
{
allFields.add(f); // Including private local fields
}
return allFields;
}
/**
* Private class to identify one class in the process of being converted, and a place
* to record objects/fields that need to be set when the conversion is complete.
*/
private static class Progress
{
public final Object source;
public final List updates;
public final String message;
public Progress(Object source, String message)
{
this.source = source;
this.updates = new Vector();
this.message = message;
}
private String addStringOf(String fieldname, String prefix)
{
try
{
Field field = source.getClass().getField(fieldname);
field.setAccessible(true);
Object value = field.get(source);
return prefix + value.toString();
}
catch (Exception e)
{
return "";
}
}
@Override
public String toString()
{
return message + " " + source.getClass().getSimpleName() +
addStringOf("name", ", name=") + addStringOf("location", " ") +
(updates.isEmpty() ? "" : ", updates " + updates.toString());
}
}
/**
* Private class to hold an object/field pair to update (see Progress class).
*
* Note that this is done by field name, because the ctor Fields in the mapping
* refer to the source class. This implies that the target ctor has to create a
* field of the same name, so that the finally clause can update the field.
*
* 99% of the time, we pop the Progress without doing a lookup anyway.
*/
private static class Pair
{
public final Object target;
public final String fieldname;
public Pair(Object object, String fieldname)
{
this.target = object;
this.fieldname = fieldname;
}
@Override
public String toString()
{
return target.getClass().getSimpleName() + "[" + fieldname + "]";
}
}
/**
* Perform a recursive mapping for a source object/tree to a destination object/tree.
*/
public T convert(Object source) throws Exception
{
String message = "Converting";
if (source instanceof List)
{
message = "Converting list";
}
else if (source instanceof Map)
{
message = "Converting map";
}
return convert(source, message);
}
/**
* Perform a recursive mapping, but clean up the converted objects. This is
* so that callers can convert small AST trees for local purposes and then
* remove them afterwards, without the accumulation of "old" entries in the
* converted map that will never be needed.
*/
public T convertLocal(Object source) throws Exception
{
try
{
mark();
return convert(source);
}
finally
{
restore();
}
}
@SuppressWarnings("unchecked")
private T convert(Object source, String message) throws Exception
{
if (source == null)
{
return null; // Can't do much else, but the type may be wrong
}
T result = null;
// Check whether we have converted the object already.
// We use the MappedObject id because the equals methods are
// not cleanly implemented, and identity collisions can occur.
// MappedObject IDs are unique for different objects.
if (source instanceof MappedObject)
{
MappedObject mo = (MappedObject)source;
result = (T)converted.get(mo.getMappedId());
if (result != null)
{
return result; // Here's the one we've already converted
}
}
try
{
inProgress.push(new Progress(source, message));
Class> srcClass = source.getClass();
MapParams mp = mappings.get(srcClass);
if (mp == null)
{
/**
* This is used for annotations, so that user supplied annotation classes
* do not fail if there is no map for a given analysis.
*/
if (source instanceof MappingOptional)
{
return null;
}
throw new Exception("No mapping for " + srcClass + " in " + configFile);
}
else if (mp.unmapped)
{
result = (T) source;
}
else
{
Object[] args = new Object[mp.ctorFields.size()];
int a = 0;
for (Field field: mp.ctorFields)
{
if (field == SELF) // ie. "this"
{
args[a++] = source;
}
else
{
field.setAccessible(true);
Object fieldvalue = field.get(source);
if (isInProgress(fieldvalue) == null)
{
args[a++] = convert(fieldvalue, "Field " + field.getName());
}
else
{
args[a++] = null;
}
}
}
result = (T) mp.constructor.newInstance(args);
int s = 0;
for (Field setter: mp.setterFields)
{
setter.setAccessible(true);
Object fieldvalue = setter.get(source);
Object arg = null;
if (isInProgress(fieldvalue) == null)
{
arg = convert(fieldvalue, "Setter " + setter.getName());
}
else
{
arg = null;
}
mp.setters[s++].invoke(result, arg);
}
for (Field field: mp.ctorFields)
{
if (field != SELF)
{
Progress progress = isInProgress(field.get(source));
if (progress != null)
{
progress.updates.add(new Pair(result, field.getName()));
}
}
}
for (Field field: mp.setterFields)
{
Progress progress = isInProgress(field.get(source));
if (progress != null)
{
progress.updates.add(new Pair(result, field.getName()));
}
}
}
}
catch (InvocationTargetException e)
{
dumpProgress(e.getCause()); // Help the user sort out where the problem is!
if (e.getCause() instanceof Exception)
{
throw (Exception)e.getCause();
}
else
{
throw new Exception(e.getCause());
}
}
finally
{
Progress progress = inProgress.pop();
if (!progress.updates.isEmpty())
{
for (Pair pair: progress.updates)
{
try
{
Field f = findField(pair.target.getClass(), pair.fieldname);
f.setAccessible(true);
f.set(pair.target, result);
}
catch (NoSuchFieldException e)
{
// This happens if a self-referencing source field is passed to the target ctor,
// but the target field name is not the same as the ctor parameter name (or the
// value isn't stored by the ctor at all). The patch-up of self-ref fields (above)
// is done by field name, so we assume the field name is the same as the ctor
// parameter name.
throw new Exception("Class " + pair.target.getClass().getSimpleName()
+ " must construct a field called " + pair.fieldname);
}
}
}
if (source instanceof MappedObject)
{
MappedObject mo = (MappedObject)source;
converted.put(mo.getMappedId(), result);
List list = marked.get(Thread.currentThread().getId());
if (list != null)
{
list.add(mo.getMappedId());
}
}
}
return result;
}
/**
* Save the "converted" map, so that we can later remove conversions
* using restore().
*/
private void mark()
{
List list = marked.get(Thread.currentThread().getId());
if (list != null)
{
errorStream.println("WARNING: Nested call to convertLocal?");
}
marked.put(Thread.currentThread().getId(), new Vector());
}
private void restore()
{
// Remove "converted" mappings from the low tide set by mark() to the
// current level.
List list = marked.get(Thread.currentThread().getId());
if (list != null)
{
for (Long id: list)
{
converted.remove(id);
}
}
marked.remove(Thread.currentThread().getId());
}
/**
* Dump the progress stack on an exception. Used in debugging.
*/
private void dumpProgress(Throwable throwable)
{
if (!dumpedProgress)
{
if (!inProgress.isEmpty())
{
errorStream.println("ClassMapper conversion stack...");
Progress top = inProgress.peek();
MapParams target = mappings.get(top.source.getClass());
for (Progress item: inProgress)
{
errorStream.println(item.toString());
}
if (target != null)
{
errorStream.println(target.toString());
}
}
errorStream.println("FAILED: " + throwable.toString());
dumpedProgress = true;
}
}
/**
* Check whether an object is already in the process of being converted.
*/
private Progress isInProgress(Object source)
{
for (Progress update: inProgress)
{
if (source == update.source)
{
return update;
}
}
return null;
}
public int getNodeCount()
{
return converted.size();
}
/**
* Return the load time of the mappings file. This is zeroed after the first request,
* because the mapping is not re-loaded after the first usage, and so the cost is
* zero.
*/
public long getLoadTime()
{
long value = loadTimeMs;
loadTimeMs = 0;
return value;
}
}