Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error in type converter CoerceUtil #689

Closed
doocaat opened this issue Aug 8, 2018 · 13 comments
Closed

Error in type converter CoerceUtil #689

doocaat opened this issue Aug 8, 2018 · 13 comments

Comments

@doocaat
Copy link

doocaat commented Aug 8, 2018

When using UUID as the primary key of the entity, when requesting data from the database, throws an exception:

java.lang.IllegalArgumentException: Parameter value [35bd1fa2-3829-11e8-8258-071bcf8a71f4] did not match expected type [java.util.UUID (n/a)]
	at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:54) ~[hibernate-core-5.3.4.Final.jar:5.3.4.Final]
	at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:27) ~[hibernate-core-5.3.4.Final.jar:5.3.4.Final]
	at org.hibernate.query.internal.QueryParameterBindingImpl.validate(QueryParameterBindingImpl.java:90) ~[hibernate-core-5.3.4.Final.jar:5.3.4.Final]
	at org.hibernate.query.internal.QueryParameterBindingImpl.setBindValue(QueryParameterBindingImpl.java:55) ~[hibernate-core-5.3.4.Final.jar:5.3.4.Final]
	at org.hibernate.query.internal.AbstractProducedQuery.setParameter(AbstractProducedQuery.java:493) ~[hibernate-core-5.3.4.Final.jar:5.3.4.Final]
	at org.hibernate.query.internal.AbstractProducedQuery.setParameter(AbstractProducedQuery.java:106) ~[hibernate-core-5.3.4.Final.jar:5.3.4.Final]
	at com.yahoo.elide.datastores.hibernate5.porting.QueryWrapper.setParameter(QueryWrapper.java:39) ~[elide-datastore-hibernate5-4.2.6.jar:na]
	at com.yahoo.elide.core.hibernate.hql.AbstractHQLQueryBuilder.lambda$supplyFilterQueryParameters$0(AbstractHQLQueryBuilder.java:108) ~[elide-datastore-hibernate-4.2.6.jar:na]
	at java.util.ArrayList.forEach(ArrayList.java:1249) ~[na:1.8.0_101]
	at com.yahoo.elide.core.hibernate.hql.AbstractHQLQueryBuilder.supplyFilterQueryParameters(AbstractHQLQueryBuilder.java:107) ~[elide-datastore-hibernate-4.2.6.jar:na]
	at com.yahoo.elide.core.hibernate.hql.RootCollectionFetchQueryBuilder.build(RootCollectionFetchQueryBuilder.java:69) ~[elide-datastore-hibernate-4.2.6.jar:na]
	at com.yahoo.elide.datastores.hibernate5.HibernateTransaction.loadObject(HibernateTransaction.java:145) ~[elide-datastore-hibernate5-4.2.6.jar:na]
	at com.yahoo.elide.core.PersistentResource.loadRecord(PersistentResource.java:225) ~[elide-core-4.2.6.jar:na]
	at com.yahoo.elide.parsers.state.StartState.handle(StartState.java:45) ~[elide-core-4.2.6.jar:na]
	at com.yahoo.elide.parsers.state.StateContext.handle(StateContext.java:64) ~[elide-core-4.2.6.jar:na]
	at com.yahoo.elide.parsers.BaseVisitor.visitRootCollectionLoadEntity(BaseVisitor.java:58) ~[elide-core-4.2.6.jar:na]
	at com.yahoo.elide.parsers.BaseVisitor.visitRootCollectionLoadEntity(BaseVisitor.java:33) ~[elide-core-4.2.6.jar:na]
	at com.yahoo.elide.generated.parsers.CoreParser$RootCollectionLoadEntityContext.accept(CoreParser.java:183) ~[elide-core-4.2.6.jar:na]
	at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visitChildren(AbstractParseTreeVisitor.java:46) ~[antlr4-runtime-4.6.jar:4.6]
	at com.yahoo.elide.generated.parsers.CoreBaseVisitor.visitStart(CoreBaseVisitor.java:20) ~[elide-core-4.2.6.jar:na]
	at com.yahoo.elide.parsers.BaseVisitor.visitStart(BaseVisitor.java:47) ~[elide-core-4.2.6.jar:na]
	at com.yahoo.elide.parsers.BaseVisitor.visitStart(BaseVisitor.java:33) ~[elide-core-4.2.6.jar:na]
	at com.yahoo.elide.generated.parsers.CoreParser$StartContext.accept(CoreParser.java:107) ~[elide-core-4.2.6.jar:na]
	at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractParseTreeVisitor.java:18) ~[antlr4-runtime-4.6.jar:4.6]
	at com.yahoo.elide.Elide.lambda$get$1(Elide.java:91) [elide-core-4.2.6.jar:na]
	at com.yahoo.elide.Elide.handleRequest(Elide.java:203) [elide-core-4.2.6.jar:na]
	at com.yahoo.elide.Elide.get(Elide.java:86) [elide-core-4.2.6.jar:na]
	at ru.macroplus.webplatform.elide.ElideControllerAutoConfiguration$ElideGetController.elideGet(ElideControllerAutoConfiguration.java:60) [classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_101]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_101]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_101]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_101]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:209) [spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:136) [spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102) [spring-webmvc-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877) [spring-webmvc-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783) [spring-webmvc-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) [spring-webmvc-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991) [spring-webmvc-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925) [spring-webmvc-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974) [spring-webmvc-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866) [spring-webmvc-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:635) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851) [spring-webmvc-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) [tomcat-embed-websocket-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:96) [spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at ru.macroplus.webplatform.auth.device.AbstractAuthenticationFilter.doFilter(AbstractAuthenticationFilter.java:42) [classes/:na]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:209) [spring-security-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178) [spring-security-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:357) [spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:270) [spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) [spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:109) [spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93) [spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200) [spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at ru.macroplus.webplatform.config.SimpleCORSFilter.doFilter(SimpleCORSFilter.java:32) [classes/:na]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:800) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:800) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1471) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_101]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_101]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.32.jar:8.5.32]
	at java.lang.Thread.run(Thread.java:745) [na:1.8.0_101]
	Suppressed: java.io.IOException: Transaction not closed
		at ru.macroplus.webplatform.elide.SpringHibernateTransaction.close(SpringHibernateTransaction.java:72) ~[classes/:na]
		at com.yahoo.elide.Elide.handleRequest(Elide.java:228) [elide-core-4.2.6.jar:na]
		... 72 common frames omitted

The problem in the class com.yahoo.elide.utils.coerce.CoerceUtil, maybe because BeanUtilsBean uses his loader and got that he is not a singleton:
BeanUtilsBean.setInstance(new BeanUtilsBean(new BidirectionalConvertUtilBean() ...

My entity:

@Entity
@Include(rootLevel = true)
public class Project {

    private UUID id;

    private String name;

    private String description;

    private Company company;

    @Id
    @Type(type = "org.hibernate.type.PostgresUUIDType")
    @Column(updatable = false, columnDefinition = "uuid DEFAULT uuid_generate_v1mc()")
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(
            name = "uuid",
            strategy = "org.hibernate.id.UUIDGenerator",
            parameters = {
                    @org.hibernate.annotations.Parameter(
                            name = "uuid_gen_strategy_class",
                            value = "ru.macroplus.webplatform.db.PostgreSQLUUIDGenerationStrategy"
                    )
            }
    )
    public UUID getId() {
        return id;
    }

    public void setId(UUID id) {
        this.id = id;
    }

    @Column(nullable = false)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Column
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @ManyToOne
    @JoinColumn(nullable = false)
    @CompanyField
    public Company getCompany() {
        return company;
    }

    public void setCompany(Company company) {
        this.company = company;
    }
}
@doocaat doocaat changed the title Ошибка в конвертере типов CoerceUtil Error in type converter CoerceUtil Aug 8, 2018
@DennisMcWherter
Copy link
Collaborator

BeanUtilsBean uses his loader and got that he is not a singleton:
BeanUtilsBean.setInstance(new BeanUtilsBean(new BidirectionalConvertUtilBean() ...

I haven't had a chance to run this under a debugger yet, but are you saying it tried to convert to UUID by using new UUID(...) instead of UUID#fromString(...)?

Or is it just providing the wrong type entirely? The constructor for UUID takes 2 long types, so I am not sure it would've been able to construct a UUID at all from a string in this way. I will take a look a bit later when I have a chance.

@doocaat
Copy link
Author

doocaat commented Aug 9, 2018

In class BeanUtilsBean register uuid converter:

private static void setup() {
        BeanUtilsBean.setInstance(new BeanUtilsBean(new BidirectionalConvertUtilBean() {
            {
                // https://github.com/yahoo/elide/issues/260
                // enable throwing exceptions when conversion fails
                register(true, false, 0);
                register(TO_UUID_CONVERTER, UUID.class);
            }...

But when I debugged the code that I understand the registered converter of type UUID disappears as bin is created anew and it already does not have a converter for UUID type.
it turns out at the time of the execution of BeanUtilSBean there is no converter: com.yahoo.elide.utils.coerce.converters.ToUUIDConverter

@clayreimann
Copy link
Contributor

Would you be willing to provide a minimal, verifiable example? This will help us in tracking down what's causing your issue.

@doocaat
Copy link
Author

doocaat commented Aug 9, 2018

To repeat the exception:
Use any entity with UUID as the primary key
And request entity of URL GET http://host/api/entity/ff9464f4-9a4c-11e8-b7fd-1392f0

@DennisMcWherter
Copy link
Collaborator

I looked into this a bit this morning. When POSTing a record, things seem to be properly converted from my end. I will try the GET request a bit later.

@DennisMcWherter
Copy link
Collaborator

Hi @stanyslav, sorry for the delay on this. I am unable to reproduce your issue without any overrides to BeanUtilsBean. Perhaps it's related to that? Would you mind sending a simple example of TO_UUID_CONVERTER?

@doocaat
Copy link
Author

doocaat commented Aug 13, 2018

Hello, i am no overrides to BeanUtilsBean.
I used default TO_UUID_CONVERTER, but it is not in the CoerceUtil instance when I call it

https://github.com/yahoo/elide/blob/master/elide-core/src/main/java/com/yahoo/elide/utils/coerce/converters/ToUUIDConverter.java

@clayreimann
Copy link
Contributor

@stanyslav we have tests for UUID primary keys generally. It looks like you’re using some Postgres specific features. Can you please provide a minimal reproducible example so we can help diagnose you’re issue?

Sent with GitHawk

@doocaat
Copy link
Author

doocaat commented Aug 13, 2018

I prepared an example with the exception of UUID
demo.zip
At the root of the project, the db_dump.sql database dump file
Open url http://localhost:6060/api/project/69f4b1ef-9f06-11e8-b7fd-630618ca6d5b
Bug in versions 4.2.5 and 4.2.6
In version 4.2.4 it works

@DennisMcWherter
Copy link
Collaborator

Hi @stanyslav, thank you for the example! I do see that I can reproduce this now with your example. After some investigation, it looks like we're getting bit by classloader issues.

You will notice that both ContextClassLoaderLocal#get and ContextClassLoaderLocal#set both call Thread.currentThread().getContextClassLoader() to get their classloader (to set the conversion properties).

The problem here is that when we start up and the static block is executed to register conversion utilities, it registers the converters with the main thread and the original classloader (i.e. Launcher$AppClassLoader, for instance in my debugger). However, by the time a worker thread handles a request, it uses Tomcat's TomcatEmbeddedWebappClassLoader classloader (for which conversion utilities have not been registered). This also explains why I could not reproduce in my other test cases because this is a classloader specific issue.

That leaves the looming question as to why this is only happening now. Namely, why in 4.2.4, does the static block initialize lazily into the correct classloader? Well, we added this serdes call and this is why this is issue now showing up. Namely, the Elide instance is built in the main thread which now calls CoerceUtils. When CoerceUtils is needed, it's pulled in by the JVM and the static initializers run in the main thread now. It just so happened (mostly by luck) that we previously only ever used CoerceUtils in the context of servicing a request. As a result, it would have only ever been loaded in the classloader used in the worker threads.

My proposed solution is to properly cope with multiple classloaders; it shouldn't be the responsibility of CoerceUtils callers to know it must be called from a single classloader. I will try to get a fix up for this soon.

@DennisMcWherter
Copy link
Collaborator

Hi @stanyslav, I have a PR here to address this issue: #693

Feel free to pull down the branch and test locally to ensure this addresses your issues. However, my local tests with your examples demonstrates to me that this will work.

@DennisMcWherter
Copy link
Collaborator

The change has been merged and should be available with release 4.2.7

@DennisMcWherter
Copy link
Collaborator

Closing issue since the fix has been merged and released. Please reopen if you experience further issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants