package com.cedarsoft.serialization.generator.intellij;
import com.cedarsoft.serialization.generator.intellij.SerializerGenerator;
import com.cedarsoft.serialization.generator.intellij.model.DelegatingSerializer;
import com.cedarsoft.serialization.generator.intellij.model.FieldSetter;
import com.cedarsoft.serialization.generator.intellij.model.FieldToSerialize;
import com.cedarsoft.serialization.generator.intellij.model.SerializerModel;
import com.intellij.codeInsight.NullableNotNullManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.project.Project;
import com.intellij.psi.JavaDirectoryService;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementFactory;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiJavaCodeReferenceElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiReferenceList;
import com.intellij.psi.PsiType;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.search.PsiShortNamesCache;
import org.jetbrains.annotations.Nullable;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* @author Johannes Schneider (<a href="mailto:js@cedarsoft.com">js@cedarsoft.com</a>)
*/
public abstract class AbstractSerializerGenerator implements SerializerGenerator {
@Nonnull
protected final Project project;
@Nonnull
protected final CodeStyleManager codeStyleManager;
@Nonnull
protected final JavaCodeStyleManager javaCodeStyleManager;
@Nonnull
protected final PsiElementFactory elementFactory;
@Nonnull
protected final JavaPsiFacade javaPsiFacade;
@Nonnull
protected final PsiShortNamesCache shortNamesCache;
@Nonnull
protected final NullableNotNullManager notNullManager;
@Nonnull
protected final String abstractSerializerType;
@Nonnull
protected final String serializeToType;
@Nonnull
protected final String deserializeFromType;
@Nonnull
protected final String serializeExceptionType;
protected AbstractSerializerGenerator( @Nonnull Project project, @Nonnull String abstractSerializerType, @Nonnull String serializeToType, @Nonnull String deserializeFromType, @Nonnull String serializeExceptionType ) {
this.project = project;
this.serializeExceptionType = serializeExceptionType;
javaCodeStyleManager = JavaCodeStyleManager.getInstance( project );
codeStyleManager = CodeStyleManager.getInstance( project );
shortNamesCache = PsiShortNamesCache.getInstance( project );
notNullManager = NullableNotNullManager.getInstance( project );
javaPsiFacade = JavaPsiFacade.getInstance( project );
elementFactory = JavaPsiFacade.getElementFactory( project );
this.abstractSerializerType = abstractSerializerType;
this.serializeToType = serializeToType;
this.deserializeFromType = deserializeFromType;
}
@Override
@Nonnull
public PsiClass generate( @Nonnull final SerializerModel serializerModel ) {
final PsiFile psiFile = serializerModel.getClassToSerialize().getContainingFile();
//The directory the serializer is generated in
final PsiDirectory directory = selectTargetDir( serializerModel.getClassToSerialize() );
final PsiClass[] serializerClass = new PsiClass[1];
new WriteCommandAction.Simple( serializerModel.getClassToSerialize().getProject(), psiFile ) {
@Override
protected void run() throws Throwable {
serializerClass[0] = JavaDirectoryService.getInstance().createClass( directory, serializerModel.generateSerializerClassName() );
fillSerializerClass( serializerModel, serializerClass[0] );
//Now beautify the code
codeStyleManager.reformat( serializerClass[0] );
javaCodeStyleManager.shortenClassReferences( serializerClass[0] );
javaCodeStyleManager.optimizeImports( serializerClass[0].getContainingFile() );
}
}.execute();
return serializerClass[0];
}
@Nonnull
protected PsiDirectory selectTargetDir( @Nonnull PsiClass psiClass ) {
//TODO implement me!
return psiClass.getContainingFile().getParent();
}
@Nonnull
public PsiClass fillSerializerClass( @Nonnull SerializerModel serializerModel, @Nonnull PsiClass serializerClass ) {
PsiClass classToSerialize = serializerModel.getClassToSerialize();
addExtends( serializerClass, classToSerialize );
addPropertyConstants( serializerModel, serializerClass );
serializerClass.add( generateConstructor( serializerModel, serializerClass ) );
serializerClass.add( generateSerializeMethod( serializerModel, serializerClass ) );
serializerClass.add( generateDeserializeMethod( serializerModel, serializerClass ) );
return serializerClass;
}
protected void addExtends( @Nonnull PsiClass serializerClass, @Nonnull PsiClass classToSerialize ) {
//Add extends abstract base class
PsiJavaCodeReferenceElement extendsRef = elementFactory.createReferenceFromText( abstractSerializerType + "<" + classToSerialize.getQualifiedName() + ">", classToSerialize );
PsiReferenceList extendsList = serializerClass.getExtendsList();
assert extendsList != null;
extendsList.add( extendsRef );
}
protected void addPropertyConstants( @Nonnull SerializerModel serializerModel, @Nonnull PsiClass serializerClass ) {
for ( FieldToSerialize entry : serializerModel.getFieldToSerializeEntries() ) {
serializerClass.add( elementFactory.createFieldFromText( "public static final String " + entry.getPropertyConstantName() + "=\"" + entry.getFieldName() + "\";", serializerClass ) );
}
}
/**
* Generates a constructor
*
* @param serializerModel the serializer model
* @return the generated constructor
*/
@Nonnull
protected PsiMethod generateConstructor( @Nonnull SerializerModel serializerModel, @Nonnull PsiClass serializerClass ) {
@Nonnull Collection<? extends DelegatingSerializer> delegatingSerializers = serializerModel.getDelegatingSerializerEntries();
StringBuilder constructorBuilder = new StringBuilder();
constructorBuilder.append( "@javax.inject.Inject public " ).append( serializerClass.getName() ).append( "(" );
//Add serializers
for ( Iterator<? extends DelegatingSerializer> iterator = delegatingSerializers.iterator(); iterator.hasNext(); ) {
DelegatingSerializer delegatingSerializer = iterator.next();
PsiType delegatingSerializerType = delegatingSerializer.getDelegatingSerializerType();
String paramName = delegatingSerializer.getSerializerParamName();
constructorBuilder
.append( notNull() )
.append( delegatingSerializerType.getCanonicalText() ).append( " " ).append( paramName );
if ( iterator.hasNext() ) {
constructorBuilder.append( "," );
}
}
callSuperConstructor( serializerModel, constructorBuilder );
//register the delegating serializers
for ( DelegatingSerializer entry : delegatingSerializers ) {
constructorBuilder.append( "getDelegatesMappings().add( " ).append( entry.getSerializerParamName() ).append( " ).responsibleFor( " ).append( entry.getSerializedTypeBoxed() ).append( ".class )" ).append( ".map( 1, 0, 0 ).toDelegateVersion( 1, 0, 0 );" );
}
if ( !delegatingSerializers.isEmpty() ) {
constructorBuilder.append( "assert getDelegatesMappings().verify();" );
}
constructorBuilder.append( "}" );
return elementFactory.createMethodFromText( constructorBuilder.toString(), null );
}
/**
* Adds the super() call
* @param serializerModel the model
* @param constructorBuilder the builder for the constructor
*/
protected abstract void callSuperConstructor( @Nonnull SerializerModel serializerModel, @Nonnull StringBuilder constructorBuilder );
/**
* Generates the serialize method
* @param serializerModel the serializer model
* @param serializerClass the serializer class
* @return the generated method
*/
@Nonnull
protected PsiElement generateSerializeMethod( @Nonnull SerializerModel serializerModel, @Nonnull PsiClass serializerClass ) {
@Nonnull PsiClass classToSerialize = serializerModel.getClassToSerialize();
@Nonnull Collection<? extends FieldToSerialize> fields = serializerModel.getFieldToSerializeEntries();
StringBuilder methodBuilder = new StringBuilder();
methodBuilder.append( "@Override public void serialize (" )
.append( notNull() ).append( serializeToType ).append( " serializeTo, " )
.append( notNull() )
.append( classToSerialize.getQualifiedName() ).append( " object," )
.append( notNull() )
.append( "com.cedarsoft.version.Version formatVersion" )
.append( ")throws java.io.IOException, com.cedarsoft.version.VersionException, " ).append( serializeExceptionType ).append( "{" );
methodBuilder.append( "verifyVersionWritable( formatVersion );" );
for ( FieldToSerialize field : fields ) {
methodBuilder.append( "serialize(object." ).append( field.getAccessor() ).append( "," ).append( field.getFieldTypeBoxed() ).append( ".class, " ).append( field.getPropertyConstantName() ).append( " , serializeTo, formatVersion);" );
}
methodBuilder.append( "}" );
return elementFactory.createMethodFromText( methodBuilder.toString(), serializerClass );
}
/**
* Generates the deserialize method
* @param serializerModel the serializer model
* @param serializerClass the serializer class
* @return the generated method
*/
@Nonnull
protected PsiElement generateDeserializeMethod( @Nonnull SerializerModel serializerModel, @Nonnull PsiClass serializerClass ) {
@Nonnull PsiClass classToSerialize = serializerModel.getClassToSerialize();
StringBuilder methodBuilder = new StringBuilder();
methodBuilder.append( "@Override " ).append( notNull() ).append( "public " ).append( classToSerialize.getQualifiedName() ).append( " deserialize(" )
.append( notNull() ).append( deserializeFromType ).append( " deserializeFrom, " )
.append( notNull() )
.append( "com.cedarsoft.version.Version formatVersion" ).append( ") throws java.io.IOException, com.cedarsoft.version.VersionException, " ).append( serializeExceptionType ).append( " {" );
methodBuilder.append( "verifyVersionWritable( formatVersion );" );
methodBuilder.append( "\n\n" );
//Appends the deserialize statements
appendDeserializeFieldStatements( serializerModel, methodBuilder );
//Create the deserialized object using the constructor
methodBuilder.append( classToSerialize.getQualifiedName() ).append( " object = new " ).append( classToSerialize.getQualifiedName() ).append( "(" );
for ( Iterator<FieldToSerialize> iterator = findConstructorArgs( serializerModel.getFieldToSerializeEntries() ).iterator(); iterator.hasNext(); ) {
FieldToSerialize constructorArgument = iterator.next();
methodBuilder.append( constructorArgument.getFieldName() );
if ( iterator.hasNext() ) {
methodBuilder.append( "," );
}
}
methodBuilder.append( ");" );
//Setting the fields using setters
for ( FieldToSerialize field : serializerModel.getFieldToSerializeEntries() ) {
FieldSetter fieldSetter = field.getFieldSetter();
if ( !fieldSetter.isSetterAccess() ) {
continue;
}
methodBuilder.append( "object." ).append( ( ( FieldSetter.SetterFieldSetter ) fieldSetter ).getSetter() ).append( "(" ).append( field.getFieldName() ).append( ");" );
}
methodBuilder.append( " return object;" );
methodBuilder.append( "}" );
return elementFactory.createMethodFromText( methodBuilder.toString(), serializerClass );
}
/**
* Append the deserialize field statements
* @param serializerModel the serializer model
* @param methodBody the method body
*/
protected abstract void appendDeserializeFieldStatements( @Nonnull SerializerModel serializerModel, @Nonnull StringBuilder methodBody );
@Nonnull
protected String notNull() {
return "@" + notNullManager.getDefaultNotNull() + " ";
}
/**
* Creates the json serializedType for the given class name
*
* @param className the class name
* @return the json serializedType
*/
@Nonnull
protected String createType( @Nonnull String className ) {
return javaCodeStyleManager.suggestVariableName( VariableKind.STATIC_FINAL_FIELD, className, null, null ).names[0].toLowerCase( Locale.getDefault() );
}
@Nonnull
public static List<FieldToSerialize> findConstructorArgs( @Nonnull Collection<? extends FieldToSerialize> fields ) {
Map<Integer, FieldToSerialize> fieldsWithConstructor = new HashMap<Integer, FieldToSerialize>();
for ( FieldToSerialize entry : fields ) {
FieldSetter fieldSetter = entry.getFieldSetter();
if ( !fieldSetter.isConstructorAccess() ) {
continue;
}
int index = ( ( FieldSetter.ConstructorFieldSetter ) fieldSetter ).getParameterIndex();
@Nullable FieldToSerialize oldValue = fieldsWithConstructor.put( index, entry );
if ( oldValue != null ) {
throw new IllegalStateException( "Duplicate entries for index <" + index + ">: " + oldValue.getFieldName() + " - " + entry.getFieldName() );
}
}
List<FieldToSerialize> argsSorted = new ArrayList<FieldToSerialize>();
int index = 0;
while ( !fieldsWithConstructor.isEmpty() ) {
@Nullable FieldToSerialize entry = fieldsWithConstructor.remove( index );
if ( entry == null ) {
throw new IllegalStateException( "No entry found for index <" + index + ">" );
}
argsSorted.add( entry );
index++;
}
return argsSorted;
}
}