I am trying to get application property object by value, i already did this in Java, but from some reason using Kotlin i can not manage to do it.
So basically what i have is list of application properties that looks like this:
ee.car.config.audi.serial=1
ee.car.config.audi.base=platform1
ee.car.config.bmw.serial=2
ee.car.config.bmw.base=platform2
so as you can see car identifiers (audi,bmw,peugeot,etc..) are dynamic, and i need simply by serial value to get object that represents the specific car and by car key(audi, bmw) to get all other properties.
And what i did is simple, i created configuration properties class like this:
#Configuration
#ConfigurationProperties(prefix = "ee.car")
data class FesEngineKeys(
val config: HashMap<String, EeCarConfigParam> = HashMap()
) {
fun getOrDefaultEEConfig(engineKey: String): EeCarConfigParam? {
return config.getOrDefault(engineKey, config["audi"])
}
And then object to map keys after dynamic value:
data class EeCarConfigParam {
var serial: String,
var base: String
}
But problem here is, in FesEngineKeys class, config property is empty, it looks like EeCarConfigParam can not be mapped.
Also interesting part is when i change:
val config: HashMap<String, EeCarConfigParam> = HashMap() to
val config: HashMap<String, String> = HashMap()
then i can see that config param is populated with all the values.
This code already works in Java and it looks like this:
#Configuration
#Getter
#Setter
#ConfigurationProperties(prefix = "ee.car")
public class FESEngineKeys {
private Map<String, EeCarConfigParam> config = new HashMap<>();
public EeCarConfigParam getOrDefaultEEConfig(String engineKey) {
return config.getOrDefault(engineKey, config.get("audi"));
}
public EeCarConfigParam findBySerial(String serial) {
return config.values().stream().filter(cfg -> cfg.getSerial().equalsIgnoreCase(serial)).findFirst().orElse(null);
}
}
#Data
public class EeCarConfigParam {
private String serial;
private String base;
}
I really don't know why in the Kotlin case it is not working, i probably made some very basic mistake, and i would really appreciate if anyone can explain what is happening here
Okay i got it.
According to that: https://docs.spring.io/spring-boot/docs/2.0.x/reference/html/boot-features-kotlin.html the support for what you want is very limited.
I made it working like that - not pretty nice :-( :
#ConfigurationProperties(prefix = "ee.car")
class FesEngineKeyProperties() {
var config: MutableMap<String, EeCarConfigParam?>? = mutableMapOf()
fun getBase(serial: String): String {
if(config == null) return ""
return config!!["audi"]?.base ?: ""
}
}
class EeCarConfigParam() {
lateinit var serial: String
lateinit var base: String
}
#SpringBootApplication
#EnableConfigurationProperties(FesEngineKeyProperties::class)
class SandboxApplication
fun main(args: Array<String>) {
runApplication<SandboxApplication>(*args)
}
I was able to handle this issue, it is somehow related to kotlin, because once when i instead of this:
data class EeCarConfigParam {
var serial: String,
var base: String
}
used "norma" Java class, everything started working, so all code from my question stays the same, only difference is this: instead of Kotlin EeCardConfigParam i created Java class like this:
public class EeCarConfigParam {
private String publicUrl;
private String base;
}
Note: with all default getters, setters, equals, hash and toString methods.
Related
I have added a data class and try to save it into Room. I went through stackoverflow and didn't find an answer.
So, the error is:
Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 2 path $
Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 2 path $
I am using Room 2.4.2 so enum is supposed to be supported.
The model I am using is :
#Entity(tableName = "userpreferencestable")
class UserPreferencesEntity (
#PrimaryKey()
var vin: String,
#ColumnInfo(name = "control")
var command: List<CommandTile?>
)
and CommandTile is defined as below:
data class CommandTile(
#SerializedName("name")
var name: DashboardTile.Name,
#SerializedName("state")
var state: DashboardTile.State
)
State and Name are enum and defined as below:
enum class Name {
NAME1,
NAME2...
}
enum class State {
TAT,
TOT
}
I have tried to add a DataConverter but it's not working.
#TypeConverter
fun fromName(name: Name): String {
return name.name
}
#TypeConverter
fun toName(name: String): Name {
return Name.valueOf(name)
}
#TypeConverter
fun fromState(state: State): String {
return state.name
}
#TypeConverter
fun toState(state: String):State {
return State.valueOf(state)
}
It still not working. I cannot figure out how to save the List of data class with enum.
Any idea ?
You issue is not the Enums, rather it is with the command List<CommandTile> (according to the disclosed code).
TypeConverters are for converting from/to a column for the data to be stored/retrieved.
As you have no #Entity annotation for the CommandTile class BUT instead have List<CommandTile?> as a column in the UserPrefrences class, which does have #Entity annotation, then Room will want to convert the List of CommandTiles to an acceptable type (in SQLite along with Room's restrictions this would have to be a type that resolves to one of TEXT (String), INTEGER (Int, Long ...), REAL (Double, Float ....) or BLOB (ByteArray).
types in parenthesise are Kotlin Types, they are examples and are not fully comprehensive.
As an example, overcoming issue that you may encounter using List, consider the following:-
A new class CommandTileList
data class CommandTileList(
val commandTileList: List<CommandTile>
)
to avoid a column that is a List
A modified UserPreferencesEntity class to use a CommandTileList rather than List<CommandTile>
#Entity(tableName = "userpreferencestable")
class UserPreferencesEntity (
#PrimaryKey()
var vin: String,
#ColumnInfo(name = "control")
var command: CommandTileList
)
the TypeConverters class, with appropriate TypeConverters
class TypeConverters {
#TypeConverter
fun fromCommandTileToString(commandTileList: CommandTileList): String {
return Gson().toJson(commandTileList)
}
#TypeConverter
fun fromStringToCommandTile(string: String): CommandTileList {
return Gson().fromJson(string,CommandTileList::class.java)
}
}
A suitable #Dao annotated class AllDAO (for demo)
#Dao
interface AllDAO {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(userPreferencesEntity: UserPreferencesEntity): Long
#Query("SELECT * FROM userpreferencestable")
fun getAllUserPreferences(): List<UserPreferencesEntity>
}
A suitable #Database annotated class TheDatabase (for demo) noting the TypeConverters class being defined with full scope via the #TypeConverters annotation (not the plural rather than singular form)
#TypeConverters(value = [TypeConverters::class])
#Database(entities = [UserPreferencesEntity::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDAO(): AllDAO
companion object {
var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance == null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
.allowMainThreadQueries for convenience/brevity
Finally putting it all into action witin an Activty MainActivity
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDAO
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val c1 = CommandTile(DashboardTile.Name.NAME1,DashboardTile.State.TAT)
val c2 = CommandTile(DashboardTile.Name.NAME2,DashboardTile.State.TOT)
db = TheDatabase.getInstance(this)
dao = db.getAllDAO()
dao.insert(userPreferencesEntity = UserPreferencesEntity("VIN1", CommandTileList(listOf(
c1,c2))))
for(u in dao.getAllUserPreferences()) {
Log.d("DBINFO","VIV = ${u.vin} CommandTiles in Command = ${u.command.commandTileList.size} They Are:-")
for (ct in u.command.commandTileList) {
Log.d("DBINFO","\tName = ${ct.name} State = ${ct.state}")
}
}
}
}
Result
The log includes:-
D/DBINFO: VIV = VIN1 CommandTiles in Command = 2 They Are:-
D/DBINFO: Name = NAME1 State = TAT
D/DBINFO: Name = NAME2 State = TOT
The Database, via App Inspection :-
As you can see the list of 2 CommandTiles (a CommandTileList) has been converted to a String (SQLite type TEXT) and stored and subsequently retrieved.
Note from a database perspective this is not ideal it
limits/complicates the usefulness of the stored data.
For example (simple) if you wanted to select all States that start with A then SELECT * FROM userpreferencestable WHERE command LIKE 'A%' would find all columns you would have to use some like SELECT * FROM userpreferencestable WHERE 'state":A%'.
introduces bloat with all that extra data and thus introduces inefficiencies.
breaks normalisation as the same values are stored multiple times
The database way would be to have a table based upon the CommandTile incorporating the suitable relationship between CommandTiles and the UserPreferences.
Then there would be no need for the TypeConverter.
To convert enum value you have to do something like this
#TypeConverter
fun fromName(name: Name): String {
return name.name
}
#TypeConverter
fun toName(name: String): Name {
return enumValueOf<Name>(name)
}
I have a model
data class SomeComplexObject<T>(val name: String, val value: T)
#Document("model")
data class SomeModel<T>(val id: String? = null, val map: Map<SomeComplexObject<T>, Int>)
Which I save to Mongo via the save method of:
#Repository
interface SomeRepo<T>: MongoRepository<SomeModel<T>, String>
On it's own, this would throw a MappingException: Cannot use a complex object as a key value. error so i'm trying to find a work around using Converters where my Converters change the Map to a List before persisting and back from List -> Map on Read.
#WritingConverter
class Map2ListConverter: Converter<Map<SomeComplexObject<*>, Int>, List<Pair<SomeComplexObject<*>, Int>>> {
override fun convert(domainModel: Map<SomeComplexObject<*>, Int>): List<Pair<SomeComplexObject<*>, Int>> {
return domainModel.map { (key, value) -> Pair(key, value) }
}
}
#ReadingConverter
class List2MapConverter: Converter<List<Pair<SomeComplexObject<*>, Int>>, Map<SomeComplexObject<*>, Int>> {
override fun convert(dbModel: List<Pair<SomeComplexObject<*>, Int>>): Map<SomeComplexObject<*>, Int> {
return dbModel.toMap()
}
}
Which I register with
#Bean
fun customConversions(): MongoCustomConversions {
val converters = ArrayList<Converter<*, *>>()
converters.add(Map2ListConverter())
converters.add(List2MapConverter())
return MongoCustomConversions(converters)
}
This however does not work & I get a CodecConfigurationException: Can't find a codec for class kotlin.Pair.. It looks like spring tries to send a Document containing my Pair class direct to Mongo which it understandably doesn't know what to do with.
Is there a way around this? Or do I need to admit defeat and just store my Maps as Sets everywhere?
For my Spring Boot application, I am trying to use an environment variable that holds the list of properties.topics in application.yml (see configuration below).
properties:
topics:
- topic-01
- topic-02
- topic-03
I use the configuration file to populate properties bean (see this spring documentation), as shown below
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
#ConfigurationProperties("properties")
public class ApplicationProperties {
private List<String> topics = new ArrayList<>();
public void setTopics(List<String> topics) {
this.topics = topics;
}
public List<String> getTopics() {
return this.topics;
}
}
With the use of environment variable, I can have the list's content change without changing the application.yml. However, all examples that I could find so far only for cases where an environment variable holding only single value, not a collection of values in my case.
Edit:
To make it clear after #vancleff's comment, I do not need the values of the environment variable to be saved to application.yml.
Another edit:
I think by oversimplifying my question, I shoot myself in the foot. #LppEdd answer works well with the example given in my question. However, what happens if instead of a collection of simple string topic names, I need a bit more complex structure. For example, something like
properties:
topics:
-
name: topic-01
id: id-1
-
name: topic-02
id: id-2
-
name: topic-03
id: id-3
a bit late for the show but, I was facing the same problem and this solves it
https://github.com/spring-projects/spring-boot/wiki/Relaxed-Binding-2.0#lists-1
MY_FOO_1_ = my.foo[1]
MY_FOO_1_BAR = my.foo[1].bar
MY_FOO_1_2_ = my.foo[1][2]`
So, for the example in the question:
properties:
topics:
-
name: topic-01
id: id-1
-
name: topic-02
id: id-2
-
name: topic-03
id: id-3
The environment variables should look like this:
PROPERTIES_TOPICS_0_NAME=topic-01
PROPERTIES_TOPICS_0_ID=id-01
PROPERTIES_TOPICS_1_NAME=topic-02
PROPERTIES_TOPICS_1_ID=id-02
PROPERTIES_TOPICS_2_NAME=topic-03
PROPERTIES_TOPICS_2_ID=id-03
Suggestion, don't overcomplicate.
Say you want that list as an Environment variable. You'd set it using
-Dtopics=topic-01,topic-02,topic-03
You then can recover it using the injected Environment Bean, and create a new List<String> Bean
#Bean
#Qualifier("topics")
List<String> topics(final Environment environment) {
final var topics = environment.getProperty("topics", "");
return Arrays.asList(topics.split(","));
}
From now on, that List can be #Autowired.
You can also consider creating your custom qualifier annotation, maybe #Topics.
Then
#Service
class TopicService {
#Topics
#Autowired
private List<String> topics;
...
}
Or even
#Service
class TopicService {
private final List<String> topics;
TopicService(#Topics final List<String> topics) {
this.topics = topics;
}
...
}
What you could do is use an externalized file.
Pass to the environment parameters the path to that file.
-DtopicsPath=C:/whatever/path/file.json
Than use the Environment Bean to recover that path. Read the file content and ask Jackson to deserialize it
You'd also need to create a simple Topic class
public class Topic {
public String name;
public String id;
}
Which represents an element of this JSON array
[
{
"name": "topic-1",
"id": "id-1"
},
{
"name": "topic-2",
"id": "id-2"
}
]
#Bean
List<Topic> topics(
final Environment environment,
final ObjectMapper objectMapper) throws IOException {
// Get the file path
final var topicsPath = environment.getProperty("topicsPath");
if (topicsPath == null) {
return Collections.emptyList();
}
// Read the file content
final var json = Files.readString(Paths.get(topicsPath));
// Convert the JSON to Java objects
final var topics = objectMapper.readValue(json, Topic[].class);
return Arrays.asList(topics);
}
Also facing the same issue , fixed with having a array in deployment.yaml from values.yml replacing the default values of application.yml
example as :
deployment.yml -
----------------
env:
- name : SUBSCRIBTION_SITES_0_DATAPROVIDER
value: {{ (index .Values.subscription.sites 0).dataprovider | quote }}
- name: SUBSCRIBTION_SITES_0_NAME
value: {{ (index .Values.subscription.sites 0).name | quote }}
values.yml -
---------------
subscription:
sites:
- dataprovider: abc
name: static
application.yml -
------------------
subscription:
sites:
- dataprovider: ${SUBSCRIBTION_SITES_0_DATAPROVIDER:abcd}
name: ${SUBSCRIBTION_SITES_0_NAME:static}
Java Code :
#Getter
#Setter
#Configuration
#ConfigurationProperties(prefix = "subscription")
public class DetailsProperties {
private List<DetailsDto> sites;
DetailsProperties() {
this.sites = new ArrayList<>();
}
}
Pojo mapped :
#Getter
#Setter
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
public class DetailsDto {
private String dataprovider;
private String name;
}
I built a quick little utility to do this.
import java.util.LinkedList;
import java.util.List;
import org.springframework.core.env.Environment;
/**
* Convenience methods for dealing with properties.
*/
public final class PropertyUtils {
private PropertyUtils() {
}
public static List<String> getPropertyArray(Environment environment, String propertyName) {
final List<String> arrayPropertyAsList = new LinkedList<>();
int i = 0;
String value;
do {
value = environment.getProperty(propertyName + "[" + i++ + "]");
if (value != null) {
arrayPropertyAsList.add(value);
}
} while (value != null);
return arrayPropertyAsList;
}
}
You could modify this without too many changes to support multiple fields as well. I've seen similar things done to load an array of database configurations from properties.
I am trying to use Jackson to serialize the same DTO object in 2 different ways, depending on the #JsonView.
I want to use 2 different names for the same field. In one case I want to name the json property myField (just like the class field name), in the other I want it to be named myInternalApiField.
As a result I would like to see outcomes similar to the presented below:
Usage 1 (External API View):
{
"myField": "value1",
"myOtherField": "otherValue"
}
Usage 2 (Internal API View):
{
"myInternalApiField": "value1",
"myOtherField": "otherValue"
}
In my implementation in Java to achieve that I used the combination of custom getters, setters and #JsonView annotation as below:
public class CustomDTO {
#JsonView(Views.ExternalApiView)
private String myField;
// Other fields here
#JsonView(Views.InternalApiView)
public String getMyInternalApiField() { return myField; }
#JsonView(Views.InternalApiView)
public void setMyInternalApiField(String value) { this.myField = value; }
#JsonView(Views.ExternalApiView)
public String getMyField() { return myField; }
#JsonView(Views.ExternalApiView)
public void setMyField(String value) { this.myField = value }
}
However I don't know how to properly achieve the same result in Kotlin.
I was thinking about using something like:
data class CustomDTO(
#get:[JsonView(Views.ExternalApiView) JsonProperty("myField")]
#get:[JsonView(Views.InternalApiView) JsonProperty("myInternalApiField")]
#set:[JsonView(Views.InternalApiView) JsonProperty("myField")]
#set:[JsonView(Views.InternalApiView) JsonProperty("myInternalApiField")]
var myField: String,
val myOtherField: String,
val myDifferentField: String
)
But this is not allowed in Kotlin.
Do you have any suggestions how to utilize the #JsonView in Kotlin in the similar way as I did it in Java?
How about something like:
data class CustomDTO(
#JsonView(ExternalApiView::class)
var myField: String,
val myOtherField: String,
val myDifferentField: String
) {
val myExternalField: String
#JsonView(InternalApiView::class)
get() {
return myField
}
}
It looks like there are ways that don't require creating computed properties in the DTO, like:
Using Jackson Mixins
Creating a custom serializer for a particular invocation
Combining a custom serializer with custom annotations
But these have their own complexity, even if that complexity isn't in the DTO class. I'm not sure these are much more appealing to me but you could see if they appeal to you.
sealed class Entity
data class Bacteria(
val uid: String,
val rank: String,
val division: String,
val scientificname: String,
val commonname: String
): Entity()
data class CTDDisease(
val diseaseId: String,
val name: String,
val altDiseaseIds: List<String>,
val parentIds: List<String>,
val definition: String?
) : Entity()
And then I want to define my document as
#Document(collection = "annotations")
data class Annotation(
#Id val id: String,
...
val spans: List<AnnotationSpan>
)
data class AnnotationSpan(
val startIndex: Int,
val endIndex: Int,
val entity: Entity? // Can be Bacteria or Disease or null
)
I also accept these 2 classes within the RequestBody from the client from time to time e.g.
#PutMapping("/annotations")
fun submitAnnotations(#RequestBody submission: Submissions): Mono<Void> { ... }
data class Submission(val annotations: List<AnnotationSpan>, ...) // AnnotationSpan contains Entity
but I get
java.lang.IllegalAccessException: class kotlin.reflect.jvm.internal.calls.CallerImpl$Constructor cannot access a member of class com.package.name.Entity with modifiers "private"
If I change the class Entity to an abstract class
abstract class Entity
then I don't get an error but my query operations keep going on forever.
Bacteria and Disease both have different fields so they should be distinguishable.
I tried using a hacky converter
#ReadingConverter
class NormalizedEntityReaderConverter: Converter<DBObject, NormalizedEntity> {
override fun convert(source: DBObject): NormalizedEntity? {
println(source.toString())
val gson = Gson()
return gson.runCatching { fromJson(source.toString(), CTDDisease::class.java) }.getOrNull()
?: gson.runCatching { fromJson(source.toString(), Bacteria::class.java) }.getOrNull()
}
}
and then registering it like
#Configuration
class MongoConverterConfig {
#Bean
fun customConversions(): MongoCustomConversions {
val normalizedEntityReaderConverter = NormalizedEntityReaderConverter()
val converterList = listOf<Converter<*, *>>(normalizedEntityReaderConverter)
return MongoCustomConversions(converterList)
}
}
My converter seems to work when called manually but for some reason, Spring still isn't picking it up.
I'm new to Spring. I would achieve this functionality in my Node.js server by using union types in TypeScript e.g.
interface AnnotationSpan {
startIndex: number;
endIndex: number;
entity?: Bacteria | Disease;
}
How can I achieve this behavior?