WildFly - getting resource from WAR - java

I am using the following method to get a resource from WAR file in WildFly:
this.getClass().getResource(relativePath)
It works when the application is deployed as exploded WAR. It used to work with compressed WAR, too. Yesterday, I did a clean and rebuild of project in Eclipse, and it just stopped working.
When I check the resource root:
logger.info(this.getClass().getResource("/").toExternalForm());
I get this:
file:/C:/JBoss/wildfly8.1.0.CR1/modules/system/layers/base/org/jboss/as/ejb3/main/timers/
So, no wonder it doesn't work. It probably has something to do with JBoss module loading, but I don't know if this is a bug or normal behavior.
I found various similar problems on StackOverflow, but no applicable solution. One of the suggestions is to use ServletContext like so:
#Resource
private WebServiceContext wsContext;
...
ServletContext servletContext = (ServletContext)this.wsContext.getMessageContext()
.get(MessageContext.SERVLET_CONTEXT);
servletContext.getResource(resourcePath);
But, when I try to obtain MessageContext in this manner, I get an IllegalStateException. So I am basically stuck. Any ideas?

I ran into this same problem, and rather than define the resource as a shared module, I ended up working around this by using a ServletContextListener in my WAR.
In the contextInitialized method, I got the ServletContext from the ServletContextEvent and used its getResource("/WEB-INF/myResource") to get the URL to the resource inside my WAR file. It appears that in the ServletContextListener, the .getResource() method resolves as expected rather than to the "/modules/system/layers/base/org/jboss/as/ejb3/main/timers/" url. That URL can then be stored in the ServletContext for later use by your servlets or in an injected ApplicationScoped CDI bean.
#WebListener
public class ServletInitializer implements ServletContextListener {
#Override
public void contextInitialized(ServletContextEvent sce) {
try {
final ServletContext context = sce.getServletContext();
final URL resourceUrl = context.getResource("/WEB-INF/myResource");
context.setAttribute("myResourceURL", resourceUrl);
} catch (final MalformedURLException e) {
throw new AssertionError("Resource not available in WAR file", e);
}
}
#Override
public void contextDestroyed(ServletContextEvent sce) {}
}
or
#WebListener
public class ServletInitializer implements ServletContextListener {
#Inject
private SomeApplicationScopedBean myBean;
#Override
public void contextInitialized(ServletContextEvent sce) {
try {
final ServletContext context = sce.getServletContext();
final URL resourceUrl = context.getResource("/WEB-INF/myResource");
myBean.setResourceUrl(resourceUrl);
} catch (final MalformedURLException e) {
throw new AssertionError("Resource not available in WAR file", e);
}
}
#Override
public void contextDestroyed(ServletContextEvent sce) {}
}

We had a similar problem and our fault was that we tried to access the static resource through the raw path instead of using the input stream the resource is providing - the following code works for us even when deploying a non-exploded .war-file.
final URL resource = this.getClass().getResource(FILE);
try (final InputStream inputStream = resource.openStream();
final InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
// Use bufferedReader to read the content
} catch (IOException e) {
// ...
}

I finally gave up and put my resource files in a new JBoss module, as described in this link.
https://community.jboss.org/wiki/HowToPutAnExternalFileInTheClasspath
It works, but the downside is that there are two deployment targets so things are more complicated. On the upside, the size of the WAR file is reduced, and I don't have to redeploy the application if only some of the resources have changed.

I was recently trying to figure out how to access a file within my own war in Java. The following is how the java classes and resources are packaged in the war file:
WAR
`-- WEB-INF
`-- classes (where all the java classes are)
`-- resourcefiles
`-- resourceFile1
My target file was resourceFile1. To get that file, I just did the following in code:
InputStream inStream = this.class.getClassLoader().getResourceAsStream("resourcefiles/resourceFile1");
In this case the resource files would need to be in the same folder as the classes folder containing the java classes. Hopefully others find this helpful.

This sample code works for wildfly deployed and tested on openshift.
I think it is a wildfly problem I downland wildfly and tried on local I also get the error.
Check sample project on github
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLConnection;
#Controller
#RequestMapping
public class FileDownloadController {
private static final Logger logger = LoggerFactory.getLogger(FileDownloadController.class);
private static final String DOC_FILE = "file/ibrahim-karayel.docx";
private static final String PDF_FILE = "file/ibrahim-karayel.pdf";
#RequestMapping(value = "/download/{type}", method = RequestMethod.GET)
public void downloadFile(HttpServletRequest request, HttpServletResponse response,
#PathVariable("type") String type) throws IOException {
File file = null;
InputStream inputStream;
if (type.equalsIgnoreCase("doc")) {
inputStream = getClass().getClassLoader().getResourceAsStream(DOC_FILE);
file = new File(Thread.currentThread().getContextClassLoader().getResource(DOC_FILE).getFile());
} else if (type.equalsIgnoreCase("pdf")) {
inputStream = getClass().getClassLoader().getResourceAsStream(PDF_FILE);
file = new File(Thread.currentThread().getContextClassLoader().getResource(PDF_FILE).getFile());
} else{
throw new FileNotFoundException();
}
if (file == null && file.getName() == null) {
logger.error("File Not Found -> " + file);
throw new FileNotFoundException();
}
String mimeType = URLConnection.guessContentTypeFromName(file.getName());
if (mimeType == null) {
System.out.println("mimetype is not detectable, will take default");
mimeType = "application/octet-stream";
}
System.out.println("mimetype : " + mimeType);
response.setContentType(mimeType);
/* "Content-Disposition : inline" will show viewable types [like images/text/pdf/anything viewable by browser] right on browser
while others(zip e.g) will be directly downloaded [may provide save as popup, based on your browser setting.]*/
response.setHeader("Content-Disposition", String.format("inline; filename=\"" + file.getName() + "\""));
/* "Content-Disposition : attachment" will be directly download, may provide save as popup, based on your browser setting*/
//response.setHeader("Content-Disposition", String.format("attachment; filename=\"%s\"", file.getName()));
response.setContentLength(inputStream.available());
IOUtils.copy(inputStream, response.getOutputStream());
response.flushBuffer();
inputStream.close();
}
}

Had the same issue with Wildfly and not-exploded WAR and using Spring and ServletContextResource I have got around it like this:
[org.springframework.core.io.]Resource resource = new ServletContextResource(servletContext, "WEB-INF/classes/resource.png");
In the same #Service class I also had:
#Inject
private ServletContext servletContext;

I decided so:
#Autowired
private final ApplicationContext ctx;
private final Path path = Paths.get("testfiles/load")
ctx.getRosource("classpath:" + path);

I read this solution, that lead us to use getResourceAsStream(...) instead of getResource() inside Wildfly. I just test it on Wildfly 19 with myApp.ear deployed from console.

Related

How to read war-file manifest in its jar-dependency?

How do I read the war-file manifest property in its jar-dependency?
UPD: servlets are not used here (it's spring-bean initialization code).
Since a .war file’s purpose is to handle servlets, I assume all of your code is called from servlets, or from a technology built on servlets, like JSP, JSF, or even Spring.
Call the current request’s getServletContext() method, and use the getResource or getResourceAsStream method of ServletContext. Those methods work just like the same methods of java.lang.Class, only they look in the .war file itself before searching the web application’s classpath for a matching path.
For example:
public Optional<Manifest> getWarManifest(ServletRequest request)
throws IOException {
InputStream manifest =
request.getServletContext().getResourceAsStream(
"/META-INF/MANIFEST.MF");
if (manifest == null) {
return Optional.empty();
}
try (InputStream stream = new BufferedInputStream(manifest)) {
return Optional.of(new Manifest(stream));
}
}
Update:
Since you want to read the manifest when preparing a Spring bean, it appears you can autowire a ServletContext object:
#Configuration
public class MyAppConfig {
#Bean
public MyBean createMyBean(#Autowired ServletContext context)
throws IOException {
Optional<Manifest> manifest;
InputStream source =
context.getResourceAsStream("/META-INF/MANIFEST.MF");
if (source == null) {
manifest = Optional.empty();
} else {
try (InputStream stream = new BufferedInputStream(source)) {
manifest = Optional.of(new Manifest(stream));
}
}
return new MyBean(manifest);
}
}

How to serve static content and resource at same base url with Grizzly

I am using Grizzly to serve my REST service which can have multiple "modules". I'd like to be able to use the same base URL for the service and for static content so I can access all these urls:
http://host:port/index.html
http://host:port/module1/index.html
http://host:port/module1/resource
http://host:port/module2/index.html
http://host:port/module2/resource
The code I'm trying to set this up with looks like this:
private HttpServer createServer(String host, int port, ResourceConfig config)
{
HttpServer server = GrizzlyHttpServerFactory.createHttpServer(URI.create("http://" + host + ":" + port + "/"), config, false);
HttpHandler httpHandler = new CLStaticHttpHandler(HttpServer.class.getClassLoader(), "docs/");
server.getServerConfiguration().addHttpHandler(httpHandler, "/");
return server;
}
With this code, I am only able to see the html pages and I get a "Resource identified by path does not exist" response when I try to get my resources.
When I comment out the code to add the HttpHandler, then I am able to access my resources (but don't have the docs of course).
What do I need to do to access both my resources and my static content?
I ended up writing a service to handle static resources myself. I decided to serve my files from the file system, but this approach would also work for serving them from a jar - you'd just have to get the file as a resource instead of creating the File directly.
#Path("/")
public class StaticService
{
#GET
#Path("/{docPath:.*}.{ext}")
public Response getHtml(#PathParam("docPath") String docPath, #PathParam("ext") String ext, #HeaderParam("accept") String accept)
{
File file = new File(cleanDocPath(docPath) + "." + ext);
return Response.ok(file).build();
}
#GET
#Path("{docPath:.*}")
public Response getFolder(#PathParam("docPath") String docPath)
{
File file = null;
if ("".equals(docPath) || "/".equals(docPath))
{
file = new File("index.html");
}
else
{
file = new File(cleanDocPath(docPath) + "/index.html");
}
return Response.ok(file).build();
}
private String cleanDocPath(String docPath)
{
if (docPath.startsWith("/"))
{
return docPath.substring(1);
}
else
{
return docPath;
}
}
}
One thing you can do is run Grizzly as a servlet container. That way you can run Jersey as servlet filter, and add a default servlet to handle the static content. For example
public class Main {
public static HttpServer createServer() {
WebappContext context = new WebappContext("GrizzlyContext", "");
createJerseyFilter(context);
createDefaultServlet(context);
HttpServer server = GrizzlyHttpServerFactory
.createHttpServer(URI.create("http://localhost:8080/"));
context.deploy(server);
return server;
}
private static void createJerseyFilter(WebappContext context) {
ResourceConfig rc = new ResourceConfig().packages("com.grizzly.test");
// This causes Jersey to forward 404s to default servlet
// which will catch all the static content requests.
rc.property(ServletProperties.FILTER_FORWARD_ON_404, true);
FilterRegistration reg = context.addFilter("JerseyApp", new ServletContainer(rc));
reg.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), "/*");
}
private static void createDefaultServlet(WebappContext context) {
ArraySet<File> baseDir = new ArraySet<>(File.class);
baseDir.add(new File("."));
ServletRegistration defaultServletReg
= context.addServlet("DefaultServlet", new DefaultServlet(baseDir) {});
defaultServletReg.addMapping("/*");
}
public static void main(String[] args) throws IOException {
HttpServer server = createServer();
System.in.read();
server.stop();
}
}
You will need to add the Jersey Grizzly servlet dependency
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-grizzly2-servlet</artifactId>
<version>${jersey2.version}</version>
</dependency>
The only problem with this approach is that the default servlet is meant to serve files from the file system, not from the classpath, as you are currently trying to do. You can see in the createDefaultServlet method I just set the base directory to the current working directory. So that's where all your files would need to be. You can change it to "docs" so all your files would be in the docs folder, which would be in the current working directory.
If you want to read files from the classpath, you may need to implement your own servlet. You can look at the source code for DefaultServlet and try to modify it to serve from the classpath. You can also check out Dropwizard's AssetServlet, which already does serve content from the classpath.
Or you can just say forget it, and just serve from the file system :-)

How to run the servlet test by using HttpUnit servlet runner? Problems in Starting up with ServletUnit?

I was planning to unit test my Servlet through ServletUnit and ran across some problems :
- As a starting point, we are supposed to create a ServletRunner object. One of the constructors expects File object with web.xml file. I provide the full path of my web.xml file but somehow it ignores the path provided and searches at the top level folder. The code-snippet and error message is below:
Code Snippet
ServletRunner sr = new ServletRunner(new File("* C:/eclipse-workspaces/pocs/lms-csd/src/main/webapp/WEB-INF/web.xml*"));
ServletUnitClient sc = sr.newClient();
WebRequest request = new PostMethodWebRequest("path to be specified" ); request.setParameter( "userId", "test" );
request.setParameter( "password", "csd" );
WebResponse response = sc.getResponse(request);
String text = response.getText();
Assert.assertTrue(text.contains("Welcome to Leave Management System"));
stack trace
com.meterware.httpunit.HttpInternalErrorException:
Error on HTTP request: 500 org.apache.jasper.JasperException: java.io.FileNotFoundException: * C:\eclipse-workspaces\pocs\lms-csd\WEB-INF\web.xml*
(The system cannot find the path specified)
[http://localhost/login] - Why does the system keep on looking at the folder structure to be .../WEB-INF/web.xml.
Mine is a maven project and I would not like to change the structure of the project to adapt this way. How can I make ServletRunner class to read from a specified folder ? - In the Servlet code,
I use the following code :
String result = null if (someCondition) result = "/welcome.jsp"; } else { logger.warn("Password Validation failed"); request.setAttribute("failedlogin", new Boolean(true)); result = "/index.jsp"; } } RequestDispatcher requestDispatcher = getServletContext().getRequestDispatcher(result); requestDispatcher.forward(request, response);
Again ServletUnit expects welcome.jsp to be at root foler, though jsp files are present at .../src/main/webapp/ folder. Again how can ServletUnit be told about the target folder location ?
Many thanks in advance.
Best Regards
M.SuriNaidu
This is the sort of thing I do. This is a facsimile of the base class of my servlet tests. In this case I pass the relative path of the web.xml file as it exists in my source tree. I run these tests from ant and eclipse.
abstract public class ServletTestCase {
protected ServletRunner m_runner;
protected ServletUnitClient m_client;
protected String m_userAgent = "something/1.0";
#Override
protected void setUp() throws Exception {
super.setUp();
initHttpUnit();
}
#Override
protected void tearDown() throws Exception {
shutdownHttpUnit();
super.tearDown();
}
protected void initHttpUnit() throws IOException, SAXException {
shutdownHttpUnit();
// We are expecting UTF-8 character handling in URLs, make it the default
HttpUnitOptions.setDefaultCharacterSet("UTF-8");
// Find the servlet's web.xml file and use it to init servletunit
File file = new File("tests/web.xml"));
m_runner = new ServletRunner(file);
m_client = m_runner.newClient();
m_client.getClientProperties().setUserAgent(m_userAgent);
}
protected void shutdownHttpUnit() {
if (m_runner != null) {
m_runner.shutDown();
}
m_client = null;
m_runner = null;
}
}

Can I dynamically discover xml files in the classpath inside an EJB 3 container?

Background:
One of the components of our project operates using spring. Some SQL code is dynamically generated, based on a given XML spring configuration.
At first it was fine to store all the XML configurations in the same package on the classpath, (and then load it as a resource when the service is called) but over time we ended up with a large number of configurations. It came time to separate the configurations into different namespaces.
The Goal
What I want is, given a starting package on the classpath, to recursively walk the directory structure and discover any spring XML files dynamically. (So that as new configurations / packages are added, the files will still be found by the service).
The Problem
I was able to accomplish my goal fine when running outside an EJB container by using Thread.getContextClassloader().getResource(myBasePackage), then getting a File object and using it to walk the tree on the filesystem. Clunky, I know, but it was still classpath relative and it worked.
However, you cannot do this inside an EJB container (you can't interact with the filesystem at all), so I had to use the rather annoying workaround in which I maintain a list of hardcoded packages to search.
The Question
Is there a way (running inside an EJB container) to dynamically walk the classpath (from a given starting location) searching for arbitrary resources?
Short answer: Not while staying in compliance with the EJB spec. Because the spec envisions containers running in all kinds of non-standard situations, it does not make this possible.
Longer answer: Since you are not creating these resources dynamically, I would write a routine that gives you a list of all of the resources at build time and puts them in a dynamically generated file that your EJB knows how to reference. So you basically create a directory listing of packages and files that you can load in the EJB that are referenced in one master file.
Spring answer: Spring supports finding resources on the classpath, although I have no idea how well this works in the EJB context (and I doubt its EJB compliant, but I haven't checked). Some details here.
DISCLAIMER: As already pointed out, creating resources in the classpath is not recommended and depending on the EJB container explicitly forbidden. This may cause you a lot of problems because containers may explode your resources into another folder or even replicate the resources throughout the cluster (if thats the case). In order to create resources dynamically you have to create a custom classloader. So, I would never do it. It is better to access the filesystem directly than the classpath. It is less ugly and eventually cluster-safe if you use a remote filesystem + file locks.
If even after all I explained you still want to play with the classpath, you can try to do something like: get the classloader via
ClassLoader cld = Thread.currentThread().getContextClassLoader();
Starting from a base package enumerate all occurrences
Enumeration<URL> basePackageUrls = cld.getResources(basePackagePath);
Each URL is generally either a file link (file:///home/scott/.../MyResource.properties) or a jar link (file:///lib.jar!/com/domain/MyResource.properties). You have to check the pattern in the URL. Using that, enumerate the contents of the folder using the normal java API and find the subpackages. Proceed until you have scanned all packages.
See the class below (will be released with an open-source project of mine soon). It implemens a classpath scanner that you can pass in a selector. It works like a visitor. It my work for you, if not, get ideas from it. See the sample annotation selector at the end.
public class ClasspathScanner
{
private static final Log log = LogFactory.getLog(ClasspathScanner.class);
private static final String JAR_FILE_PATTERN = ".jar!";
private ClassSelector selector;
private Set<Class<?>> classes;
// PUBLIC METHODS ------------------------------------------------------------------------------
public synchronized Set<Class<?>> scanPackage(String basePackage, ClassSelector selector)
throws Exception
{
if (selector == null)
{
throw new NullPointerException("Selector cannot be NULL");
}
this.selector = selector;
this.classes = new HashSet<Class<?>>();
Set<Class<?>> aux;
try
{
scanClasses0(basePackage);
aux = this.classes;
}
finally
{
this.selector = null;
this.classes = null;
}
return aux;
}
// HELPER CLASSES ------------------------------------------------------------------------------
private void scanClasses0(String basePackage)
throws IOException, ClassNotFoundException, FileNotFoundException
{
File packageDirectory = null;
ClassLoader cld = getLoader();
String basePackagePath = basePackage.replace('.', '/');
Enumeration<URL> basePackageUrls = cld.getResources(basePackagePath);
if (basePackageUrls == null || !basePackageUrls.hasMoreElements())
{
throw new ClassNotFoundException("Base package path not found: [" + basePackagePath
+ "]");
}
while (basePackageUrls.hasMoreElements())
{
String packagePath = basePackageUrls.nextElement().getFile();
if (packagePath.contains(JAR_FILE_PATTERN))
{
scanJarFile(basePackagePath, packagePath);
}
else
{
packageDirectory = new File(packagePath);
scanDirectory(basePackage, packageDirectory);
}
}
}
private void scanDirectory(String packageName, File packagePath)
throws ClassNotFoundException, FileNotFoundException
{
if (packagePath.exists())
{
File[] packageFiles = packagePath.listFiles();
for (File file : packageFiles)
{
if (file.isFile() && file.getName().endsWith(".class"))
{
String fullFileName = packageName + '.' + file.getName();
checkClass(fullFileName);
}
else if (file.isDirectory())
{
scanDirectory(packageName + "." + file.getName(), file);
}
}
}
else
{
throw new FileNotFoundException(packagePath.getPath());
}
}
private void scanJarFile(String basePackagePath, String jarFileUrl)
throws IOException, ClassNotFoundException
{
String jarFilePath = jarFileUrl.substring("file:".length(), jarFileUrl
.indexOf(JAR_FILE_PATTERN)
+ JAR_FILE_PATTERN.length() - 1);
log.debug("URL JAR file path: [" + jarFilePath + "]");
jarFilePath = URLDecoder.decode(jarFilePath, "UTF-8");
log.debug("Decoded JAR file path: [" + jarFilePath + "]");
JarFile jar = new JarFile(new File(jarFilePath));
for (Enumeration<JarEntry> jarFiles = jar.entries(); jarFiles.hasMoreElements();)
{
JarEntry file = jarFiles.nextElement();
String fileName = file.getName();
if (!file.isDirectory() && fileName.endsWith(".class")
&& fileName.startsWith(basePackagePath))
{
String className = fileName.replace('/', '.');
checkClass(className);
}
}
}
private void checkClass(String fullFilePath) throws ClassNotFoundException
{
String className = fullFilePath.substring(0, fullFilePath.length() - 6);
Class<?> c = getLoader().loadClass(className);
if (selector.select(c))
{
classes.add(c);
}
}
private ClassLoader getLoader()
{
ClassLoader loader = Thread.currentThread().getContextClassLoader();
if (loader == null)
{
loader = getClass().getClassLoader();
}
return loader;
}
// INNER CLASSES -------------------------------------------------------------------------------
public interface ClassSelector
{
boolean select(Class<?> clazz);
}
public static class AnnotatedClassSelector implements ClassSelector
{
private final Class<? extends Annotation>[] annotations;
public AnnotatedClassSelector(Class<? extends Annotation>... annotations)
{
this.annotations = annotations;
}
public boolean select(Class<?> clazz)
{
for (Class<? extends Annotation> ac : annotations)
{
if (clazz.isAnnotationPresent(ac))
{
return true;
}
}
return false;
}
}
}

getSystemResourceAsStream() returns null

Hiii...
I want to get the content of properties file into InputStream class object using getSystemResourceAsStream(). I have built the sample code. It works well using main() method,but when i deploy the project and run on the server, properties file path cannot obtained ... so inputstream object store null value.
Sample code is here..
public class ReadPropertyFromFile {
public static Logger logger = Logger.getLogger(ReadPropertyFromFile.class);
public static String readProperty(String fileName, String propertyName) {
String value = null;
try {
//fileName = "api.properties";
//propertyName = "api_loginid";
System.out.println("11111111...In the read proprty file.....");
// ClassLoader loader = ClassLoader.getSystemClassLoader();
InputStream inStream = ClassLoader.getSystemResourceAsStream(fileName);
System.out.println("In the read proprty file.....");
System.out.println("File Name :" + fileName);
System.out.println("instream = "+inStream);
Properties prop = new Properties();
try {
prop.load(inStream);
value = prop.getProperty(propertyName);
} catch (Exception e) {
logger.warn("Error occured while reading property " + propertyName + " = ", e);
return null;
}
} catch (Exception e) {
System.out.println("Exception = " + e);
}
return value;
}
public static void main(String args[]) {
System.out.println("prop value = " + ReadPropertyFromFile.readProperty("api.properties", "api_loginid"));
}
}
i deploy the project and run on the server,
This sounds like a JSP/Servlet webapplication. In that case, you need to use the ClassLoader which is obtained as follows:
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
This one has access to the all classpath paths tied to the webapplication in question and you're not anymore dependent on which parent classloader (a webapp has more than one!) has loaded your class.
Then, on this classloader, you need to just call getResourceAsStream() to get a classpath resource as stream, not the getSystemResourceAsStream() which is dependent on how the webapplication is started. You don't want to be dependent on that as well since you have no control over it at external hosting:
InputStream input = classLoader.getResourceAsStream("filename.extension");
This is finally more robust than your initial getSystemResourceAsStream() approach and the Class#getResourceAsStream() as suggested by others.
The SystemClassLoader loads resources from java.class.path witch maps to the system variable CLASSPATH. In your local application, you probably have the resource your trying to load configured in java.class.path variable. In the server, it's another story because most probably the server loads your resources from another class loader.
Try using the ClassLoader that loaded class using the correct path:
getClass().getResourceAsStream(fileName);
This article might also be useful.
Try using getResourceAsStream() instead of getSystemResourceAsStream().

Categories

Resources