I got a tricky issue concerning collection filtering in kotlin...
I got a base class that manages a list of items and I want to be able to filter the list with a keyword so I extended the class with Filterable methods.
What I want to do is to be able to extend multiple classes with this 'base class' so the filter mecanism is the same for all classes.
These classes don't have the same properties... In one, the filtering must occur depending if the keyword is found in the 'name' while in another class the filtering is done on the 'comment' property.
Here some code:
data class ProductInfo(): {
var _name: String
var name: String
get() = _name
set(value) { _name = value }
}
abstract class BaseFirestoreAdapter<T : BaseFirestoreAdapter.DataInterface, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>(), Filterable
{
var sourceList: MutableList<ProductInfo> = ArrayList()
...
override fun performFiltering(keyword: CharSequence): FilterResults {
val keywordRegex = keyword.toString().toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.LITERAL))
filteredList = sourceList.filter {
keywordRegex.containsMatchIn(Normalizer.normalize(it.name, Normalizer.Form.NFD).replace("[^\\p{ASCII}]".toRegex(RegexOption.IGNORE_CASE), ""))
}
results.values = filteredList.sortedWith(orderComparator)
results.count = filteredList.size
}
...
}
I developped the 'base class' so it works with the first class mentionned above (filtering is done with the 'it.name') and it works but now that I'm trying to make it generic (T) to use it with the second class (comments), I can't find a way to do it...
I thought I could pass a class related predicate defining how to match the items during the filtering but since the keyword is only known in the performFiltering method, I can't create properly the predicate outside of this method...
I'm kinda out of ideas now! lol
Any of you have an idea?
UPDATE: Following #Tenfour04's suggestion, I tried adapting it to my code which passes filtering predicates via a method instead of using the constructor but it does not compile unless I replace "ActivyInfo::comments" with something like "ActivyInfo::comments.name" but then the
value I get for "searchedProperty(it)" in debug is "name" which is not the comment value.
Here is the code:
CommentAdapter:
override fun getFilter(): Filter {
super.setFilter(
{ it.state != ProductState.HIDDEN },
{ ActivyInfo::comments },
compareBy<ProductInfo> { it.state }.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name })
return super.getFilter()
}
BaseAdapter:
lateinit var defaultFilterPredicate : (T) -> Boolean
lateinit var searchedProperty : (T) -> CharSequence
lateinit var orderComparator : Comparator<T>
fun setFilter(defaultPredicate: (T) -> Boolean, property: (T) -> CharSequence, comparator: Comparator<T> ) {
defaultFilterPredicate = defaultPredicate
searchedProperty = property
orderComparator = comparator
}
override fun performFiltering(constraint: CharSequence): FilterResults {
...
filteredList = sourceList.filter {
constraintRegex.containsMatchIn(Normalizer.normalize(searchedProperty(it), Normalizer.Form.NFD).replace("[^\\p{ASCII}]".toRegex(RegexOption.IGNORE_CASE), ""))
}
...
}
You can pass into the constructor a parameter that specifies the property as a function.
abstract class BaseFirestoreAdapter<T : BaseFirestoreAdapter.DataInterface, VH : RecyclerView.ViewHolder>(val filteredProperty: (T) -> CharSequence) : RecyclerView.Adapter<VH>(), Filterable
{
var sourceList: MutableList<T> = ArrayList()
// ...
override fun performFiltering(keyword: CharSequence): FilterResults {
val keywordRegex = keyword.toString().toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.LITERAL))
filteredList = sourceList.filter {
keywordRegex.containsMatchIn(Normalizer.normalize(filteredProperty(it), Normalizer.Form.NFD).replace("[^\\p{ASCII}]".toRegex(RegexOption.IGNORE_CASE), ""))
}
results.values = filteredList.sortedWith(orderComparator)
results.count = filteredList.size
}
...
}
The changes I made to yours were adding the constructor parameter filteredProperty, Changing the sourceList type to T, and replacing it.name with filteredProperty(it).
So subclasses will have to call this super-constructor, passing the property in like this:
data class SomeData(val comments: String)
class SomeDataAdapter: BaseFirestoreAdapter<SomeData>(SomeData::comments) {
//...
}
Or if you want to keep it generic:
class SomeDataAdapter(filteredProperty: (T) -> CharSequence): BaseFirestoreAdapter<SomeData>(filteredProperty) //...
Related
I have an authorizaton Aspect that checks specific conditions based on method annotation.
This specific example shows annotation to mark a method that is only accessible by customer service. But unfortunately this isn't the only condition.
We have to confirm the customerServiceId that is also passed as one of method parameters. The parameter containing customerServiceId is pretty nested, so I was wondering if it's possible to get parameter value by some kind of a "path".
So let's say we have this method:
fun updateRemoteConfig(val remoteConfig: RemoteConfig) { doSomething() }
RemoteConfig class is pretty nested, so the path to customerServiceId would be something like: remoteConfig.customerService.id
What I would like to achieve is mark the method with annotation:
#CustomerServiceAccess(customerServiceIdPath = "remoteConfig.customerService.id")
And the value would then be fetched inside Aspect method. But I have no idea how to get to the specified value by path. Is it even possible?
The unknown is where arrows are in the code. Here's rest of the aspect:
#Aspect
class AuthorizationAspect {
#Pointcut("#annotation(com.my.project.annotations.CustomerServiceAccess)")
fun customerServiceAccess() = Unit
#Before("customerServiceAccess()")
fun checkAccess(joinPoint: JoinPoint) {
val methodSignature = joinPoint.signature as MethodSignature
val method = methodSignature.method
val canAccess = mutableListOf<() -> Boolean>()
.apply {
addAll(method.getAnnotationsByType(CustomerServiceAccess::class.java).map { it.canAccess(method) })
}
.any { it() }
if (!canAccess) {
throw UnauthorizedException(message = "User cannot perform this action")
}
}
private fun CustomerServiceAccess.canAccess(val method: Method): () -> Boolean = {
->> val customerServiceIdParam = method.getParameterByPath(getCustomerServiceIdPath())
SecurityContext.isCustomerService && SecurityContext.customerServiceId == customerServiceIdParam
}
private fun CustomerServiceAccess.getCustomerServiceIdPath(): String = this.customerServiceIdPath
}
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FUNCTION)
annotation class CustomerServiceAccess(val customerServiceIdPath: String)
Is there a way to register a codec for multiple classes? Basically, all my classes should just be serialized using a Jackson object mapper. But it seems like I have to create a custom codec for each class (even though I can abstract it a little bit using generics).
A small code example:
Codec:
class JacksonCodec<T>(private val mapper: ObjectMapper, private val clazz: Class<T>) : MessageCodec<T, T> {
override fun encodeToWire(buffer: Buffer, s: T) {
buffer.appendBytes(mapper.writeValueAsBytes(s))
}
override fun decodeFromWire(pos: Int, buffer: Buffer): T {
val length = buffer.getInt(pos)
val bytes = buffer.getBytes(pos + 4, pos + 4 + length)
return mapper.readValue(bytes, clazz)
}
...
}
register codec for each class I want to serialize:
vertx.eventBus()
.registerDefaultCodec(A::class.java, JacksonCodec(DatabindCodec.mapper(), A::class.java))
vertx.eventBus()
vertx.eventBus()
.registerDefaultCodec(B::class.java, JacksonCodec(DatabindCodec.mapper(), B::class.java))
vertx.eventBus()
The code examples are kotlin but same applies for Java.
As far as I can tell looking at the code, there is no way, as the class needs to be the exact match:
https://github.com/eclipse-vertx/vert.x/blob/master/src/main/java/io/vertx/core/eventbus/impl/CodecManager.java#L99
It is possible, with some limitations and quirks. I would not recommend doing it.
Let's start with the limitations:
It can not be used in clustered mode
You have to declare the codec name every time you send something over the eventbus.
If you create a generic codec that encodes classes with Jackson and every time you send something over the eventbus you make sure to add it using codecName in the deliveryOptions, you can register it only once and use it for all of your classes.
Full example:
fun main() {
val vertx = Vertx.vertx()
vertx.eventBus().registerCodec(GenericCodec())
vertx.eventBus().consumer<Foo>("test-address") {
println(it.body())
it.reply(Bar(), genericDeliveryOptions)
}
vertx.eventBus().request<String>("test-address", Foo(), genericDeliveryOptions) {
println(it.result().body())
}
vertx.close()
}
data class Foo(
val foo: String = "foo",
)
data class Bar(
val bar: String = "bar",
)
class GenericCodec : MessageCodec<Any, Any> {
companion object {
const val NAME = "generic"
}
private val mapper: ObjectMapper = ObjectMapper()
override fun encodeToWire(buffer: Buffer, s: Any) {
buffer.appendBytes(mapper.writeValueAsBytes(s))
}
override fun decodeFromWire(pos: Int, buffer: Buffer): Any {
throw RuntimeException("should never get here, unless using clustered mode")
}
override fun transform(s: Any): Any {
return s
}
override fun name(): String {
return NAME
}
override fun systemCodecID(): Byte {
return -1
}
}
val genericDeliveryOptions = deliveryOptionsOf(codecName = GenericCodec.NAME)
I have a problem with the inclusion and exclusion of nested fields in the aggregation.. I have a collection that contains nested objects, so when I try to build the query by including only some fields of nested objects it only works with the query, but it doesn't work with aggregation
This is a simple preview of my collection
{
"id": "1234",
"name": "place name",
"address": {
"city": "city name",
"gov": "gov name",
"country": "country name",
"location": [0.0, 0.0],
//some other data
},
//some other data
}
when i configure my fields with query it works
query.fields().include("name").include("address.city").include("address.gov")
Also when I do it with aggregation using the shell it works
db.getCollection("places").aggregate([
{ $project: {
"name": 1,
"address.city": 1,
"address.gov": 1
} },
])
but it doesn't work with aggregation in spring
val aggregation = Aggregation.newAggregation(
Aggregation.match(criteria)
Aggregation.project().andInclude("name").andInclude("address.city").andInclude("address.gov")
)
This aggregation with spring always returns address field as null, but when I put the "address" field without its nested fields the result will contain the full address object, not just its nested fields that I want to include.
Can someone tell me how to fix that?
I found a solution, it's by using the nested() function
val aggregation = Aggregation.newAggregation(
Aggregation.match(criteria)
Aggregation.project().andInclude("name")
.and("address").nested(Fields.fields("address.city", "address.gov"))
)
But it only works with hardcoded fields.. So if you want to have a function to which you pass the list of fields to include or exclude, you can use this solution
fun fieldsConfig(fields : List<String>) : ProjectionOperation
{
val mainFields = fields.filter { !it.contains(".") }
var projectOperation = Aggregation.project().andInclude(*mainFields.toTypedArray())
val nestedFields = fields.filter { it.contains(".") }.map { it.substringBefore(".") }.distinct()
nestedFields.forEach { mainField ->
val subFields = fields.filter { it.startsWith("${mainField}.") }
projectOperation = projectOperation.and(mainField).nested(Fields.fields(*subFields.toTypedArray()))
}
return projectOperation
}
The problem with this solution is that there is a lot of code, memory allocation for objects, and configuration to include fields.. In addition, it works only with inclusion, if you use it to exclude fields, it throws an exception.. Also, it does not work with the deepest fields of your document.
So I implemented a simpler and more elegant solution, which covers most cases of inclusion and exclusion of fields.
This builder class allows you to create an object containing the fields you want to include or exclude.
class DbFields private constructor(private val list : List<String>, val include : Boolean) : List<String>
{
override val size : Int get() = list.size
//overridden functions of List class.
/**
* the builder of the fields.
*/
class Builder
{
private val list = ArrayList<String>()
private var include : Boolean = true
/**
* add a new field.
*/
fun withField(field : String) : Builder
{
list.add(field)
return this
}
/**
* add a new fields.
*/
fun withFields(fields : Array<String>) : Builder
{
fields.forEach {
list.add(it)
}
return this
}
fun include() : Builder
{
include = true
return this
}
fun exclude() : Builder
{
include = false
return this
}
fun build() : DbFields
{
if (include && !list.contains("id"))
{
list.add("id")
}
else if (!include && list.contains("id"))
{
list.remove("id")
}
return DbFields(list.distinct(), include)
}
}
}
To build your fields configuration
val fields = DbFields.Builder()
.withField("fieldName")
.withField("fieldName")
.withField("fieldName")
.include()
.build()
This object you can pass it to your repository to configure the inclusion or exlusion.
I also created this class to configures the inclusion and exclusion using raw documents that will be transformed to a custom aggregation operation.
class CustomProjectionOperation(private val fields : DbFields) : ProjectionOperation()
{
override fun toDocument(context : AggregationOperationContext) : Document
{
val fieldsDocument = BasicDBObject()
fields.forEach {
fieldsDocument.append(it, fields.include)
}
val operation = Document()
operation.append("\$project", fieldsDocument)
return operation
}
}
now, you have just to use this class in your aggregation
class RepoCustomImpl : RepoCustom
{
#Autowired
private lateint mongodb : MongoTemplate
override fun getList(fields : DbFields) : List<Result>
{
val aggregation = Aggregation.newAggregation(
CustomProjectionOperation(fields)
)
return mongodb.aggregate(aggregation, Result::class.java, Result::class.java).mappedResults
}
}
I have this arraylist of GameObjects. I loop through the arraylist, and if the type of the object is door (one of the GameObject's child classes), and if some other conditions match up, i want to call a function from the door class thats only in that class. Is this possible? I'm using Kotlin, but if you only know java i could probably port it.
You can use is, as? or with operators combined with smart casts for that.
In java you can code as below:
for (GameObject gameObject: GameObjects) {
if(gameObject instanceof Door ) { // you can add your another condition in this if itself
// your implementation for the door object will come here
}
}
You can use like this:
//Kotlin 1.1
interface GameObject {
fun age():Int
}
class GameObjectDoor(var age: Int) : GameObject{
override fun age():Int = age;
override fun toString():String = "{age=$age}";
}
fun main(args: Array<String>) {
val gameObjects:Array<GameObject> = arrayOf(
GameObjectDoor(1),
GameObjectDoor(2),
GameObjectDoor(3));
for (item: GameObject in gameObjects) {
when (item) {
is GameObjectDoor -> {
var door = item as GameObjectDoor
println(door)
//do thomething with door
}
//is SomeOtherClass -> {do something}
}
}
}
I am very new to Gson and Json. I have simple Events that I want to serialize through Json with the help of Gson.
Note: Code in Kotlin.
public abstract class Event() {
}
public class Move : Event() {
var from: Point? = null
var to: Point? = null
}
public class Fire : Event() {
var damage: Int = 0
var area: ArrayList<Point> = ArrayList(0)
}
public class Build : Event() {
var to: Point? = null
var type: String = ""
var owner: String = ""
}
I am persisting bunch of these via this way:
val list: ArrayList<Event>() = ArrayList()
list.add(move)
list.add(fire)
val str = gson.toJson(events)
And unpersisting:
val type = object : TypeToken<ArrayList<Event>>(){}.getType()
val eventStr = obj.getString("events")
val events: ArrayList<Event> = gson.fromJson(eventStr, type)
I have tried both creating a serializer & deserializer for Event-class, and registering it via registerTypeAdapter, and I have also tried the RuntimeTypeAdapterFactory, but neither will persist the information required to unpersist the correct type.
For example, the RuntimeTypeAdapterFactory says:
"cannot deserialize Event because it does not define a field named type"
EDIT: Here's the code for the "Adapter", which was.. well, adapted from another StackOverflow post:
public class Adapter :
JsonSerializer<Event>,
JsonDeserializer<Event> {
final val CLASSNAME = "CLASSNAME"
final val INSTANCE = "INSTANCE"
override fun serialize(src: Event?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement? {
val obj = JsonObject()
val className = (src as Event).javaClass.getCanonicalName()
obj.addProperty(CLASSNAME, className)
val elem = context!!.serialize(src)
obj.add(INSTANCE, elem)
return obj
}
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Event? {
val jsonObject = json!!.getAsJsonObject()
val prim = jsonObject.get(CLASSNAME)
val className = prim.getAsString()
val klass = Class.forName(className)
return context!!.deserialize(jsonObject.get(INSTANCE), klass)
}
}
This code fails with NullPointerException on line:
val className = prim.getAsString()
You can't do it this way.
The example you are referring is not targeted to your case. It works in only one case: if you register base type (not type hierarchy) and serialize using gson.toJson(obj, javaClass<Event>()). It will never work for array except you write custom serializer for you events container object too
Generally you need another approach: use TypeAdapterFactory and delegate adapters: GSON: serialize/deserialize object of class, that have registered type hierarchy adapter, using ReflectiveTypeAdapterFactory.Adapter and https://code.google.com/p/google-gson/issues/detail?id=43#c15
I believe this approach is overcomplicated so if you have few types the easiest solution is two serialize these types by hand, field by field via custom serializer and forget about attempts to delegate to default