This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to annotated tag org.apache.sling.fsresource-0.9.2-incubator in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-fsresource.git
commit 0c3311850cedd91ea42f581f66b7bf9c494d30e4 Author: Felix Meschberger <[email protected]> AuthorDate: Fri Jul 25 13:53:57 2008 +0000 SLING-583 Initial implementation of a filesystem resource provider git-svn-id: https://svn.apache.org/repos/asf/incubator/sling/trunk/samples/fsresource@679807 13f79535-47bb-0310-9956-ffa450edef68 --- pom.xml | 110 +++++++++++ .../apache/sling/fsprovider/FsFolderServlet.java | 160 ++++++++++++++++ .../sling/fsprovider/FsProviderConstants.java | 46 +++++ .../org/apache/sling/fsprovider/FsResource.java | 164 ++++++++++++++++ .../sling/fsprovider/FsResourceProvider.java | 211 +++++++++++++++++++++ .../OSGI-INF/metatype/metatype.properties | 40 ++++ 6 files changed, 731 insertions(+) diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..14dbb95 --- /dev/null +++ b/pom.xml @@ -0,0 +1,110 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.sling</groupId> + <artifactId>sling</artifactId> + <version>4-incubator-SNAPSHOT</version> + <relativePath>../../parent/pom.xml</relativePath> + </parent> + + <artifactId>org.apache.sling.fsresource</artifactId> + <packaging>bundle</packaging> + <version>0.9.0-incubator-SNAPSHOT</version> + + <name>Sling - Filesystem Resource Provider</name> + <description> + Provides a ResourceProvider implementation supporting filesystem + based resources. + </description> + + <scm> + <connection>scm:svn:http://svn.apache.org/repos/asf/incubator/sling/trunk/samples/fsresource</connection> + <developerConnection>scm:svn:https://svn.apache.org/repos/asf/incubator/sling/trunk/samples/fsresource</developerConnection> + <url>http://svn.apache.org/viewvc/incubator/sling/trunk/samples/fsresource</url> + </scm> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-scr-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Private-Package> + org.apache.sling.fsprovider.* + </Private-Package> + </instructions> + </configuration> + </plugin> + </plugins> + </build> + <reporting> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <configuration> + <excludePackageNames> + org.apache.sling.bundleresource + </excludePackageNames> + </configuration> + </plugin> + </plugins> + </reporting> + <dependencies> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>servlet-api</artifactId> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.api</artifactId> + <version>2.0.2-incubator</version> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.adapter</artifactId> + <version>2.0.2-incubator</version> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.osgi.core</artifactId> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.osgi.compendium</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + </dependency> + </dependencies> +</project> diff --git a/src/main/java/org/apache/sling/fsprovider/FsFolderServlet.java b/src/main/java/org/apache/sling/fsprovider/FsFolderServlet.java new file mode 100644 index 0000000..351525f --- /dev/null +++ b/src/main/java/org/apache/sling/fsprovider/FsFolderServlet.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.fsprovider; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.SlingHttpServletResponse; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceNotFoundException; +import org.apache.sling.api.resource.ResourceUtil; +import org.apache.sling.api.servlets.SlingSafeMethodsServlet; + +/** + * The <code>FsFolderServlet</code> lists the files and folders of a folder + * mapped into the Sling resource tree. The listing produced is similar to the + * default index listing produced by Apache httpd. + * + * @scr.component immediate="true" metatype="no" + * @scr.service interface="javax.servlet.Servlet" + * @scr.property name="service.description" value="FileSystem Folder Servlet" + * @scr.property name="service.vendor" value="The Apache Software Foundation" + * @scr.property name="sling.servlet.methods" value="GET" + * @scr.property name="sling.servlet.resourceTypes" + * valueRef="FsProviderConstants.RESOURCE_TYPE_FOLDER" + */ +public class FsFolderServlet extends SlingSafeMethodsServlet { + + // a number of blanks used to format the listing of folder entries + private static final String NAME_BLANKS = " "; + + @Override + protected void doGet(SlingHttpServletRequest request, + SlingHttpServletResponse response) throws IOException { + + // if the request URL is not terminated with a slash, redirect to the + // same URL with a trailing slash (this makes preparing the response + // easier + if (!request.getRequestURI().endsWith("/")) { + response.sendRedirect(request.getRequestURL() + "/"); + return; + } + + // ensure the resource adapts to a filesystem folder; generally + // this should be the case, but we never know whether someone really + // creates a JCR resource with the fs provider folder resource type + Resource res = request.getResource(); + File file = res.adaptTo(File.class); + if (file == null || !file.isDirectory()) { + throw new ResourceNotFoundException( + request.getResource().getPath(), + "Resource is not a file system folder"); + } + + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + PrintWriter pw = response.getWriter(); + + pw.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\"> <html>"); + + pw.printf("<head><title>Index of %s</title></head><body>%n", res.getPath()); + pw.printf("<h1>Index of %s</h1>%n", res.getPath()); + + pw.println("<pre>"); + pw.println("Name Last modified Size Description"); + pw.println("<hr>"); + + // only draw parent link if the parent is also a fs resource + Resource parent = ResourceUtil.getParent(res); + if (parent != null && parent.adaptTo(File.class) != null) { + pw.println("<a href='..'>Parent Directory</a>"); + } + + // render the children + renderChildren(pw, file); + + pw.println("</pre>"); + pw.println("</body></html>"); + } + + // ---------- internal + + /** + * Renders the children of the <code>parent</code> folder to the output. + */ + private void renderChildren(PrintWriter pw, File parent) { + File[] children = parent.listFiles(); + if (children != null && children.length > 0) { + Arrays.sort(children, FileNameComparator.INSTANCE); + + for (File child : children) { + + String name = child.getName(); + if (child.isDirectory()) { + name = name.concat("/"); + } + + String displayName = name; + if (displayName.length() >= 32) { + displayName = displayName.substring(0, 29).concat("..."); + pw.printf("<a href='%s'>%s</a>", name, displayName); + } else { + String blanks = NAME_BLANKS.substring(0, + 32 - displayName.length()); + pw.printf("<a href='%s'>%s</a>%s", name, displayName, + blanks); + } + + pw.print(" " + new Date(child.lastModified())); + + pw.print(" "); + if (child.isFile()) { + pw.print(child.length()); + } else { + pw.print("-"); + } + + pw.println(); + } + } + } + + // order files by type (folder before files) and name (case insensitive) + private static class FileNameComparator implements Comparator<File> { + + public static final FileNameComparator INSTANCE = new FileNameComparator(); + + public int compare(File f1, File f2) { + if (f1.isDirectory() && !f2.isDirectory()) { + return -1; + } else if (!f1.isDirectory() && f2.isDirectory()) { + return 1; + } + + return f1.getName().compareToIgnoreCase(f2.getName()); + } + } + +} diff --git a/src/main/java/org/apache/sling/fsprovider/FsProviderConstants.java b/src/main/java/org/apache/sling/fsprovider/FsProviderConstants.java new file mode 100644 index 0000000..abdf459 --- /dev/null +++ b/src/main/java/org/apache/sling/fsprovider/FsProviderConstants.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.fsprovider; + +/** + * The <code>FsProviderConstants</code> interface defines public constants for + * the {@link FsResourceProvider}. + */ +public interface FsProviderConstants { + + /** + * The common resource super type for files and folders mapped into the + * resource tree by the {@link FsResourceProvider} (value is + * "sling/fs/resource"). + */ + static final String RESOURCE_TYPE_ROOT = "sling/fs/resource"; + + /** + * The resource type for file system files mapped into the resource tree by + * the {@link FsResourceProvider} (value is "sling/fs/file"). + */ + static final String RESOURCE_TYPE_FILE = "sling/fs/file"; + + /** + * The resource type for file system folders mapped into the resource tree + * by the {@link FsResourceProvider} (value is "sling/fs/folder"). + */ + static final String RESOURCE_TYPE_FOLDER = "sling/fs/folder"; + +} diff --git a/src/main/java/org/apache/sling/fsprovider/FsResource.java b/src/main/java/org/apache/sling/fsprovider/FsResource.java new file mode 100644 index 0000000..f17a40d --- /dev/null +++ b/src/main/java/org/apache/sling/fsprovider/FsResource.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.fsprovider; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; + +import org.apache.sling.adapter.SlingAdaptable; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceMetadata; +import org.apache.sling.api.resource.ResourceResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The <code>FsResource</code> represents a file system file or folder as + * a Sling Resource. + */ +public class FsResource extends SlingAdaptable implements Resource { + + // default log, assigned on demand + private Logger log; + + // the owning resource resolver + private final ResourceResolver resolver; + + // the path of this resource in the resource tree + private final String resourcePath; + + // the file wrapped by this instance + private final File file; + + // the resource type, assigned on demand + private String resourceType; + + // the resource metadata, assigned on demand + private ResourceMetadata metaData; + + /** + * Creates an instance of this Filesystem resource. + * + * @param resolver The owning resource resolver + * @param resourcePath The resource path in the resource tree + * @param file The wrapped file + */ + FsResource(ResourceResolver resolver, String resourcePath, File file) { + this.resolver = resolver; + this.resourcePath = resourcePath; + this.file = file; + } + + /** + * Returns the path of this resource + */ + public String getPath() { + return resourcePath; + } + + /** + * Returns the resource meta data for this resource containing the file + * length, last modification time and the resource path (same as + * {@link #getPath()}). + */ + public ResourceMetadata getResourceMetadata() { + if (metaData == null) { + metaData = new ResourceMetadata(); + metaData.setContentLength(file.length()); + metaData.setModificationTime(file.lastModified()); + metaData.setResolutionPath(resourcePath); + } + return metaData; + } + + /** + * Returns the resource resolver which cause this resource object to be + * created. + */ + public ResourceResolver getResourceResolver() { + return resolver; + } + + /** + * Returns {@link FsProviderConstants#RESOURCE_TYPE_ROOT} + */ + public String getResourceSuperType() { + return FsProviderConstants.RESOURCE_TYPE_ROOT; + } + + /** + * Returns {@link FsProviderConstants#RESOURCE_TYPE_FILE} if this resource + * wraps a file. Otherwise {@link FsProviderConstants#RESOURCE_TYPE_FOLDER} + * is returned. + */ + public String getResourceType() { + if (resourceType == null) { + resourceType = file.isFile() + ? FsProviderConstants.RESOURCE_TYPE_FILE + : FsProviderConstants.RESOURCE_TYPE_FOLDER; + } + + return resourceType; + } + + /** + * Returns an adapter for this resource. This implementation supports + * <code>File</code>, <code>InputStream</code> and <code>URL</code> + * plus those supported by the adapter manager. + */ + @SuppressWarnings("unchecked") + public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) { + if (type == File.class) { + + return (AdapterType) file; + + } else if (type == InputStream.class && file.canRead()) { + + try { + return (AdapterType) new FileInputStream(file); + } catch (IOException ioe) { + getLog().info("Cannot open a stream on the file " + file, ioe); + } + + } else if (type == URL.class) { + + try { + return (AdapterType) file.toURI().toURL(); + } catch (MalformedURLException mue) { + getLog().info( + "Cannot convert the file path " + file + " to an URL", mue); + } + } + + return super.adaptTo(type); + } + + // ---------- internal + + private Logger getLog() { + if (log == null) { + log = LoggerFactory.getLogger(getClass()); + } + return log; + } +} diff --git a/src/main/java/org/apache/sling/fsprovider/FsResourceProvider.java b/src/main/java/org/apache/sling/fsprovider/FsResourceProvider.java new file mode 100644 index 0000000..daa3401 --- /dev/null +++ b/src/main/java/org/apache/sling/fsprovider/FsResourceProvider.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.fsprovider; + +import java.io.File; +import java.util.Dictionary; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceProvider; +import org.apache.sling.api.resource.ResourceResolver; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; + +/** + * The <code>FsResourceProvider</code> is a resource provider which maps + * filesystem files and folders into the virtual resource tree. The provider is + * implemented in terms of a component factory, that is multiple instances of + * this provider may be created by creating respective configuration. + * <p> + * Each provider instance is configured with to properties: The location in the + * resource tree where resources are provided ({@link ResourceProvider#ROOTS}) + * and the file system path from where files and folders are mapped into the + * resource ({@link #PROP_PROVIDER_FILE}). + * + * @scr.component label="%resource.resolver.name" + * description="%resource.resolver.description" + * factory="org.apache.sling.fsprovider.FsResourceProviderFactory" + * @scr.service + * @scr.property name="service.description" value="Sling Filesystem Resource + * Provider" + * @scr.property name="service.vendor" value="The Apache Software Foundation" + * @scr.property nameRef="ResourceProvider.ROOTS" + * @scr.property nameRef="PROP_PROVIDER_FILE" + */ +public class FsResourceProvider implements ResourceProvider { + + /** + * The name of the configuration property providing file system path of + * files and folders mapped into the resource tree (value is + * "provider.file"). + */ + public static final String PROP_PROVIDER_FILE = "provider.file"; + + // The location in the resource tree where the resources are mapped + private String providerRoot; + + // The "root" file or folder in the file system + private File providerFile; + + /** + * Same as {@link #getResource(ResourceResolver, String)}, i.e. the + * <code>request</code> parameter is ignored. + * + * @see #getResource(ResourceResolver, String) + */ + public Resource getResource(ResourceResolver resourceResolver, + HttpServletRequest request, String path) { + return getResource(resourceResolver, path); + } + + /** + * Returns a resource wrapping a filesystem file or folder for the given + * path. If the <code>path</code> is equal to the configured resource tree + * location of this provider, the configured file system file or folder is + * used for the resource. Otherwise the configured resource tree location + * prefix is removed from the path and the remaining relative path is used + * to access the file or folder. If no such file or folder exists, this + * method returns <code>null</code>. + */ + public Resource getResource(ResourceResolver resourceResolver, String path) { + File file; + if (path.equals(providerRoot)) { + file = providerFile; + } else { + String relPath = path.substring(providerRoot.length() + 1); + file = new File(providerFile, relPath); + } + + if (file.exists()) { + return new FsResource(resourceResolver, path, file); + } + + // not applicable or not an existing file path + return null; + } + + /** + * Returns an iterator of resources. + */ + public Iterator<Resource> listChildren(Resource parent) { + File parentFile = parent.adaptTo(File.class); + if (parentFile == null) { + // not a FsResource, try to create one from the resource + parent = getResource(parent.getResourceResolver(), parent.getPath()); + if (parent != null) { + parentFile = parent.adaptTo(File.class); + } + } + + if (parentFile != null) { + + final File[] children = parentFile.listFiles(); + + if (children != null && children.length > 0) { + final ResourceResolver resolver = parent.getResourceResolver(); + final String parentPath = parent.getPath(); + return new Iterator<Resource>() { + int index = 0; + + public boolean hasNext() { + return index < children.length; + } + + public Resource next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + File file = children[index]; + index++; + + return new FsResource(resolver, parentPath + "/" + + file.getName(), file); + } + + public void remove() { + throw new UnsupportedOperationException("remove"); + } + }; + } + } + + // no children + return null; + } + + // ---------- SCR Integration + + protected void activate(ComponentContext context) { + Dictionary<?, ?> props = context.getProperties(); + + String providerRoot = (String) props.get(ROOTS); + if (providerRoot == null || providerRoot.length() == 0) { + throw new IllegalArgumentException(ROOTS + " property must be set"); + } + + String providerFileName = (String) props.get(PROP_PROVIDER_FILE); + if (providerFileName == null || providerFileName.length() == 0) { + throw new IllegalArgumentException(PROP_PROVIDER_FILE + + " property must be set"); + } + + this.providerRoot = providerRoot; + this.providerFile = getProviderFile(providerFileName, + context.getBundleContext()); + } + + protected void deactivate(ComponentContext context) { + this.providerRoot = null; + this.providerFile = null; + } + + // ---------- internal + + private File getProviderFile(String providerFileName, + BundleContext bundleContext) { + + // the file object from the plain name + File providerFile = new File(providerFileName); + + // resolve relative file name against sling.home or current + // working directory + if (!providerFile.isAbsolute()) { + String home = bundleContext.getProperty("sling.home"); + if (home != null && home.length() > 0) { + providerFile = new File(home, providerFileName); + } + } + + // resolve the path + providerFile = providerFile.getAbsoluteFile(); + + // if the provider file does not exist, create an empty new folder + if (!providerFile.exists() && !providerFile.mkdirs()) { + throw new IllegalArgumentException( + "Cannot create provider file root " + providerFile); + } + + return providerFile; + } +} diff --git a/src/main/resources/OSGI-INF/metatype/metatype.properties b/src/main/resources/OSGI-INF/metatype/metatype.properties new file mode 100644 index 0000000..1e2ec6b --- /dev/null +++ b/src/main/resources/OSGI-INF/metatype/metatype.properties @@ -0,0 +1,40 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + + +# +# This file contains localization strings for configuration labels and +# descriptions as used in the metatype.xml descriptor generated by the +# the SCR plugin + +# +# Localizations for FsResourceProvider configuration +resource.resolver.name = Filesystem Resource Provider +resource.resolver.description = Configure an instance of the filesystem \ + resource provider in terms of provider root and filesystem location + +provider.roots.name = Provider Root +provider.roots.description = Location in the virtual resource tree where the \ + filesystem resources are mapped in. This property must not be an empty string. +provider.file.name = Filesystem Root +provider.file.description = Filesystem directory mapped to the virtual \ + resource tree. This property must not be an empty string. If the path is \ + relative it is resolved against sling.home or the current working directory. \ + The path may be a file or folder. If the path does not address an existing \ + file or folder, an empty folder is created. \ No newline at end of file -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
