mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-07 10:04:21 +00:00
DS-3489: Move DSpaceResource links to LinkFactory
This commit is contained in:
@@ -2,14 +2,19 @@
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
* <p>
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
package org.dspace.app.rest;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import javax.naming.Context;
|
||||
import javax.naming.InitialContext;
|
||||
import javax.servlet.Filter;
|
||||
|
||||
import org.dspace.app.rest.filter.DSpaceRequestContextFilter;
|
||||
import org.dspace.app.rest.model.hateoas.DSpaceRelProvider;
|
||||
import org.dspace.app.rest.parameter.resolver.SearchFilterResolver;
|
||||
import org.dspace.app.rest.utils.ApplicationConfig;
|
||||
import org.dspace.app.util.DSpaceContextListener;
|
||||
import org.dspace.kernel.DSpaceKernel;
|
||||
@@ -28,11 +33,12 @@ import org.springframework.boot.web.servlet.ServletContextInitializer;
|
||||
import org.springframework.boot.web.support.SpringBootServletInitializer;
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.event.ContextClosedEvent;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.hateoas.RelProvider;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.context.request.RequestContextListener;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
@@ -59,16 +65,12 @@ import java.io.File;
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class Application extends SpringBootServletInitializer {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Application.class);
|
||||
|
||||
@Autowired
|
||||
private ApplicationConfig configuration;
|
||||
|
||||
@Value("${dspace.dir}")
|
||||
private String dspaceHome;
|
||||
|
||||
private transient DSpaceKernelImpl kernelImpl;
|
||||
|
||||
/**
|
||||
* Override the default SpringBootServletInitializer.configure() method,
|
||||
* passing it this Application class.
|
||||
@@ -84,6 +86,9 @@ public class Application extends SpringBootServletInitializer {
|
||||
*/
|
||||
@Override
|
||||
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
|
||||
return application.sources(Application.class)
|
||||
.initializers(new DSpaceKernelInitializer());
|
||||
}
|
||||
|
||||
// start the kernel when the webapp starts
|
||||
try {
|
||||
@@ -112,14 +117,7 @@ public class Application extends SpringBootServletInitializer {
|
||||
|
||||
@Bean
|
||||
public ServletContextInitializer contextInitializer() {
|
||||
return new ServletContextInitializer() {
|
||||
|
||||
@Override
|
||||
public void onStartup(ServletContext servletContext)
|
||||
throws ServletException {
|
||||
servletContext.setInitParameter("dspace.dir", configuration.getDspaceHome());
|
||||
}
|
||||
};
|
||||
return servletContext -> servletContext.setInitParameter("dspace.dir", configuration.getDspaceHome());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,7 +169,7 @@ public class Application extends SpringBootServletInitializer {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebMvcConfigurer webMvcConfigurer() {
|
||||
public WebMvcConfigurer corsConfigurer() {
|
||||
return new WebMvcConfigurerAdapter() {
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
@@ -180,41 +178,10 @@ public class Application extends SpringBootServletInitializer {
|
||||
registry.addMapping("/api/**").allowedOrigins(corsAllowedOrigins);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
|
||||
argumentResolvers.add(new SearchFilterResolver());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find DSpace's "home" directory.
|
||||
* Initially look for JNDI Resource called "java:/comp/env/dspace.dir".
|
||||
* If not found, look for "dspace.dir" initial context parameter.
|
||||
*/
|
||||
private String getProvidedHome(String dspaceHome) {
|
||||
String providedHome = null;
|
||||
try {
|
||||
Context ctx = new InitialContext();
|
||||
providedHome = (String) ctx.lookup("java:/comp/env/" + DSpaceConfigurationService.DSPACE_HOME);
|
||||
} catch (Exception e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
if (providedHome == null) {
|
||||
if (dspaceHome != null && !dspaceHome.equals("") &&
|
||||
!dspaceHome.equals("${" + DSpaceConfigurationService.DSPACE_HOME + "}")) {
|
||||
File test = new File(dspaceHome);
|
||||
if (test.exists() && new File(test, DSpaceConfigurationService.DSPACE_CONFIG_PATH).exists()) {
|
||||
providedHome = dspaceHome;
|
||||
}
|
||||
}
|
||||
return providedHome;
|
||||
}
|
||||
return providedHome;
|
||||
}
|
||||
|
||||
/** Utility class that will destroy the DSpace Kernel on Spring Boot shutdown */
|
||||
private class DSpaceKernelDestroyer implements ApplicationListener<ContextClosedEvent> {
|
||||
private DSpaceKernelImpl kernelImpl;
|
||||
|
||||
@@ -229,4 +196,67 @@ public class Application extends SpringBootServletInitializer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Utility class that will initialize the DSpace Kernel on Spring Boot startup */
|
||||
private class DSpaceKernelInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||
|
||||
private transient DSpaceKernelImpl kernelImpl;
|
||||
|
||||
public void initialize(final ConfigurableApplicationContext applicationContext) {
|
||||
|
||||
String dspaceHome = applicationContext.getEnvironment().getProperty("dspace.dir");
|
||||
|
||||
// start the kernel when the webapp starts
|
||||
try {
|
||||
this.kernelImpl = DSpaceKernelInit.getKernel(null);
|
||||
if (!this.kernelImpl.isRunning()) {
|
||||
this.kernelImpl.start(getProvidedHome(dspaceHome)); // init the kernel
|
||||
}
|
||||
|
||||
//Set the DSpace Kernel Application context as a parent of the Spring Boot context so that
|
||||
//we can auto-wire all DSpace Kernel services
|
||||
applicationContext.setParent(kernelImpl.getServiceManager().getApplicationContext());
|
||||
|
||||
//Add a listener for Spring Boot application shutdown so that we can nicely cleanup the DSpace kernel.
|
||||
applicationContext.addApplicationListener(new DSpaceKernelDestroyer(kernelImpl));
|
||||
|
||||
} catch (Exception e) {
|
||||
// failed to start so destroy it and log and throw an exception
|
||||
try {
|
||||
this.kernelImpl.destroy();
|
||||
} catch (Exception e1) {
|
||||
// nothing
|
||||
}
|
||||
String message = "Failure during ServletContext initialisation: " + e.getMessage();
|
||||
log.error(message + ":" + e.getMessage(), e);
|
||||
throw new RuntimeException(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find DSpace's "home" directory.
|
||||
* Initially look for JNDI Resource called "java:/comp/env/dspace.dir".
|
||||
* If not found, look for "dspace.dir" initial context parameter.
|
||||
*/
|
||||
private String getProvidedHome(String dspaceHome) {
|
||||
String providedHome = null;
|
||||
try {
|
||||
Context ctx = new InitialContext();
|
||||
providedHome = (String) ctx.lookup("java:/comp/env/" + DSpaceConfigurationService.DSPACE_HOME);
|
||||
} catch (Exception e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
if (providedHome == null) {
|
||||
if (dspaceHome != null && !dspaceHome.equals("") &&
|
||||
!dspaceHome.equals("${" + DSpaceConfigurationService.DSPACE_HOME + "}")) {
|
||||
File test = new File(dspaceHome);
|
||||
if (test.exists() && new File(test, DSpaceConfigurationService.DSPACE_CONFIG_PATH).exists()) {
|
||||
providedHome = dspaceHome;
|
||||
}
|
||||
}
|
||||
}
|
||||
return providedHome;
|
||||
}
|
||||
}
|
||||
}
|
@@ -26,6 +26,7 @@ import org.dspace.app.rest.exception.PaginationException;
|
||||
import org.dspace.app.rest.exception.RepositoryNotFoundException;
|
||||
import org.dspace.app.rest.exception.RepositorySearchMethodNotFoundException;
|
||||
import org.dspace.app.rest.exception.RepositorySearchNotFoundException;
|
||||
import org.dspace.app.rest.link.HalLinkService;
|
||||
import org.dspace.app.rest.model.LinkRest;
|
||||
import org.dspace.app.rest.model.RestModel;
|
||||
import org.dspace.app.rest.model.hateoas.DSpaceResource;
|
||||
@@ -74,6 +75,9 @@ public class RestResourceController implements InitializingBean {
|
||||
@Autowired
|
||||
RestRepositoryUtils repositoryUtils;
|
||||
|
||||
@Autowired
|
||||
HalLinkService linkService;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
List<Link> links = new ArrayList<Link>();
|
||||
@@ -124,6 +128,7 @@ public class RestResourceController implements InitializingBean {
|
||||
throw new ResourceNotFoundException(apiCategory + "." + model + " with id: " + id + " not found");
|
||||
}
|
||||
DSpaceResource result = repository.wrapResource(modelObject);
|
||||
linkService.addLinks(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -213,6 +218,8 @@ public class RestResourceController implements InitializingBean {
|
||||
}
|
||||
RestModel modelObject = repository.findOne(uuid);
|
||||
DSpaceResource result = repository.wrapResource(modelObject, rel);
|
||||
linkService.addLinks(result);
|
||||
|
||||
if (result.getLink(rel) == null) {
|
||||
// TODO create a custom exception
|
||||
throw new ResourceNotFoundException(rel + "undefined for " + model);
|
||||
@@ -251,6 +258,7 @@ public class RestResourceController implements InitializingBean {
|
||||
Page<DSpaceResource<T>> resources;
|
||||
try {
|
||||
resources = repository.findAll(page).map(repository::wrapResource);
|
||||
resources.forEach(linkService::addLinks);
|
||||
} catch (PaginationException pe) {
|
||||
resources = new PageImpl<DSpaceResource<T>>(new ArrayList<DSpaceResource<T>>(), page, pe.getTotal());
|
||||
}
|
||||
@@ -320,6 +328,7 @@ public class RestResourceController implements InitializingBean {
|
||||
ResourceSupport result = null;
|
||||
if (returnPage) {
|
||||
Page<DSpaceResource<T>> resources = ((Page<T>) searchResult).map(repository::wrapResource);
|
||||
resources.forEach(linkService::addLinks);
|
||||
result = assembler.toResource(resources, link);
|
||||
} else {
|
||||
result = repository.wrapResource((T) searchResult);
|
||||
|
@@ -7,38 +7,75 @@
|
||||
*/
|
||||
package org.dspace.app.rest.link;
|
||||
|
||||
import org.dspace.app.rest.DiscoveryRestController;
|
||||
import org.dspace.app.rest.model.hateoas.DSpaceResource;
|
||||
import org.dspace.app.rest.model.hateoas.HALResource;
|
||||
import org.dspace.app.rest.model.hateoas.SearchConfigurationResource;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Links;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.beans.IntrospectionException;
|
||||
import java.beans.Introspector;
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dspace.app.rest.RestResourceController;
|
||||
import org.dspace.app.rest.model.LinkRest;
|
||||
import org.dspace.app.rest.model.RestModel;
|
||||
import org.dspace.app.rest.model.hateoas.DSpaceResource;
|
||||
import org.dspace.app.rest.utils.Utils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Created by raf on 25/09/2017.
|
||||
*/
|
||||
@Component
|
||||
public class DSpaceResourceHalLinkFactory extends HalLinkFactory<DSpaceResource, DiscoveryRestController> {
|
||||
public class DSpaceResourceHalLinkFactory extends HalLinkFactory<DSpaceResource, RestResourceController> {
|
||||
|
||||
@Autowired
|
||||
private Utils utils;
|
||||
|
||||
protected void addLinks(DSpaceResource halResource, LinkedList<Link> list) {
|
||||
RestModel data = halResource.getData();
|
||||
|
||||
try {
|
||||
for (PropertyDescriptor pd : Introspector.getBeanInfo(data.getClass()).getPropertyDescriptors()) {
|
||||
Method readMethod = pd.getReadMethod();
|
||||
String name = pd.getName();
|
||||
if (readMethod != null && !"class".equals(name)) {
|
||||
LinkRest linkAnnotation = readMethod.getAnnotation(LinkRest.class);
|
||||
|
||||
if (linkAnnotation != null) {
|
||||
if (StringUtils.isNotBlank(linkAnnotation.name())) {
|
||||
name = linkAnnotation.name();
|
||||
}
|
||||
|
||||
Link linkToSubResource = utils.linkToSubResource(data, name);
|
||||
// no method is specified to retrieve the linked object(s) so check if it is already here
|
||||
if (StringUtils.isBlank(linkAnnotation.method())) {
|
||||
halResource.add(linkToSubResource);
|
||||
}
|
||||
|
||||
} else if (RestModel.class.isAssignableFrom(readMethod.getReturnType())) {
|
||||
Link linkToSubResource = utils.linkToSubResource(data, name);
|
||||
halResource.add(linkToSubResource);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IntrospectionException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
halResource.add(utils.linkToSingleResource(data, Link.REL_SELF));
|
||||
}
|
||||
|
||||
protected Class<DiscoveryRestController> getControllerClass() {
|
||||
return null;
|
||||
protected Class<RestResourceController> getControllerClass() {
|
||||
return RestResourceController.class;
|
||||
}
|
||||
|
||||
protected Class<DSpaceResource> getResourceClass() {
|
||||
return null;
|
||||
return DSpaceResource.class;
|
||||
}
|
||||
|
||||
protected String getSelfLink(DSpaceResource halResource) {
|
||||
return null;
|
||||
return utils.linkToSingleResource(halResource.getData(), Link.REL_SELF).getHref();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -7,18 +7,17 @@
|
||||
*/
|
||||
package org.dspace.app.rest.link;
|
||||
|
||||
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
|
||||
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.dspace.app.rest.model.hateoas.HALResource;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
|
||||
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
|
||||
|
||||
/**
|
||||
* Created by raf on 25/09/2017.
|
||||
*/
|
||||
@@ -26,7 +25,7 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
|
||||
public abstract class HalLinkFactory<RESOURCE, CONTROLLER> {
|
||||
|
||||
public boolean supports(Class clazz) {
|
||||
if(Objects.equals(clazz, getResourceClass())){
|
||||
if(getResourceClass().isAssignableFrom(clazz)){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
package org.dspace.app.rest.link;
|
||||
|
||||
import org.dspace.app.rest.DiscoveryRestController;
|
||||
import org.dspace.app.rest.model.hateoas.DSpaceResource;
|
||||
import org.dspace.app.rest.model.hateoas.HALResource;
|
||||
import org.dspace.app.rest.model.hateoas.ItemResource;
|
||||
import org.dspace.app.rest.model.hateoas.SearchConfigurationResource;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Links;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by raf on 25/09/2017.
|
||||
*/
|
||||
@Component
|
||||
public class ItemResourceHalLinkFactory extends HalLinkFactory<ItemResource, DiscoveryRestController> {
|
||||
|
||||
protected void addLinks(ItemResource halResource, LinkedList<Link> list) {
|
||||
|
||||
}
|
||||
|
||||
protected Class<DiscoveryRestController> getControllerClass() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected Class<ItemResource> getResourceClass() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String getSelfLink(ItemResource halResource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@@ -7,21 +7,13 @@
|
||||
*/
|
||||
package org.dspace.app.rest.link.search;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.dspace.app.rest.DiscoveryRestController;
|
||||
import org.dspace.app.rest.link.HalLinkFactory;
|
||||
import org.dspace.app.rest.model.SearchConfigurationRest;
|
||||
import org.dspace.app.rest.model.hateoas.HALResource;
|
||||
import org.dspace.app.rest.model.hateoas.SearchConfigurationResource;
|
||||
import org.dspace.app.rest.model.hateoas.SearchSupportResource;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
|
||||
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
|
||||
|
||||
/**
|
||||
* Created by raf on 25/09/2017.
|
||||
@@ -32,7 +24,7 @@ public class SearchSupportHalLinkFactory extends HalLinkFactory<SearchSupportRes
|
||||
protected void addLinks(SearchSupportResource halResource, LinkedList<Link> list) {
|
||||
list.add(buildLink(Link.REL_SELF, getMethodOn()
|
||||
.getSearchSupport(null, null)));
|
||||
list.add(buildLink("configuration", getMethodOn().getSearchConfiguration(null, null)));
|
||||
list.add(buildLink("search", getMethodOn().getSearchConfiguration(null, null)));
|
||||
list.add(buildLink("facets", getMethodOn().getFacetsConfiguration(null, null)));
|
||||
}
|
||||
|
||||
|
@@ -7,6 +7,16 @@
|
||||
*/
|
||||
package org.dspace.app.rest.model.hateoas;
|
||||
|
||||
import java.beans.IntrospectionException;
|
||||
import java.beans.Introspector;
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dspace.app.rest.model.BaseObjectRest;
|
||||
@@ -20,16 +30,6 @@ import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.hateoas.Link;
|
||||
|
||||
import java.beans.IntrospectionException;
|
||||
import java.beans.Introspector;
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A base class for DSpace Rest HAL Resource. The HAL Resource wraps the REST
|
||||
* Resource adding support for the links and embedded resources. Each property
|
||||
@@ -99,7 +99,6 @@ public abstract class DSpaceResource<T extends RestModel> extends HALResource {
|
||||
Link linkToSubResource = utils.linkToSubResource(data, name);
|
||||
// no method is specified to retrieve the linked object(s) so check if it is already here
|
||||
if (StringUtils.isBlank(linkAnnotation.method())) {
|
||||
|
||||
Object linkedObject = readMethod.invoke(data);
|
||||
Object wrapObject = linkedObject;
|
||||
if (linkedObject instanceof RestModel) {
|
||||
@@ -168,8 +167,6 @@ public abstract class DSpaceResource<T extends RestModel> extends HALResource {
|
||||
}
|
||||
}
|
||||
else if (RestModel.class.isAssignableFrom(readMethod.getReturnType())) {
|
||||
Link linkToSubResource = utils.linkToSubResource(data, name);
|
||||
this.add(linkToSubResource);
|
||||
RestModel linkedObject = (RestModel) readMethod.invoke(data);
|
||||
if (linkedObject != null) {
|
||||
embedded.put(name,
|
||||
@@ -188,15 +185,9 @@ public abstract class DSpaceResource<T extends RestModel> extends HALResource {
|
||||
| InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
this.add(utils.linkToSingleResource(data, Link.REL_SELF));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(Link link) {
|
||||
System.out.println("Chiamato "+link.getRel());
|
||||
super.add(link);
|
||||
}
|
||||
public Map<String, Object> getEmbedded() {
|
||||
return embedded;
|
||||
}
|
||||
|
Reference in New Issue
Block a user