merge with default branch

This commit is contained in:
Sebastian Sdorra
2020-03-04 12:58:53 +01:00
21 changed files with 172 additions and 184 deletions

View File

@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- New footer design
- Update jgit to version 5.6.1.202002131546-r-scm1
- Update svnkit to version 1.10.1-scm1
### Fixed
@@ -21,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
- Enunciate rest documentation
- Obsolete fields in data transfer objects
## 2.0.0-rc4 - 2020-02-14
### Added

View File

@@ -856,7 +856,7 @@
<shiro.version>1.4.1</shiro.version>
<!-- repository libraries -->
<jgit.version>v5.4.0.201906121030-r-scm2</jgit.version>
<jgit.version>5.6.1.202002131546-r-scm1</jgit.version>
<svnkit.version>1.10.1-scm1</svnkit.version>
<!-- util libraries -->

View File

@@ -51,7 +51,7 @@ public class GitConfigResource {
@GET
@Path("")
@Produces(GitVndMediaType.GIT_CONFIG)
@Operation(summary = "Git configuration", description = "Returns the global git configuration.", tags = "Git")
@Operation(summary = "Git configuration", description = "Returns the global git configuration.", tags = "Git", operationId = "git_get_config")
@ApiResponse(
responseCode = "200",
description = "success",
@@ -91,11 +91,8 @@ public class GitConfigResource {
@PUT
@Path("")
@Consumes(GitVndMediaType.GIT_CONFIG)
@Operation(summary = "Modify git configuration", description = "Modifies the global git configuration.", tags = "Git")
@ApiResponse(
responseCode = "204",
description = "update success"
)
@Operation(summary = "Modify git configuration", description = "Modifies the global git configuration.", tags = "Git", operationId = "git_put_config")
@ApiResponse(responseCode = "204", description = "update success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:git\" privilege")
@ApiResponse(

View File

@@ -4,54 +4,34 @@ import com.google.common.io.ByteStreams;
import org.eclipse.jgit.attributes.FilterCommand;
import org.eclipse.jgit.attributes.FilterCommandRegistry;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.SystemReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.plugin.Extension;
import javax.inject.Inject;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.regex.Pattern;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
@Extension
public class GitLfsFilterContextListener implements ServletContextListener {
public static final String GITCONFIG = "[filter \"lfs\"]\n" +
"clean = git-lfs clean -- %f\n" +
"smudge = git-lfs smudge -- %f\n" +
"process = git-lfs filter-process\n" +
"required = true\n";
public static final Pattern COMMAND_NAME_PATTERN = Pattern.compile("git-lfs (smudge|clean) -- .*");
private static final Logger LOG = LoggerFactory.getLogger(GitLfsFilterContextListener.class);
private final SCMContextProvider contextProvider;
@Inject
public GitLfsFilterContextListener(SCMContextProvider contextProvider) {
this.contextProvider = contextProvider;
}
@Override
public void contextInitialized(ServletContextEvent sce) {
Path gitconfig = contextProvider.getBaseDirectory().toPath().resolve("gitconfig");
try {
Files.write(gitconfig, GITCONFIG.getBytes(Charset.defaultCharset()), TRUNCATE_EXISTING, CREATE);
FS.DETECTED.setGitSystemConfig(gitconfig.toFile());
LOG.info("wrote git config file: {}", gitconfig);
} catch (IOException e) {
LOG.error("could not write git config in path {}; git lfs support may not work correctly", gitconfig, e);
SystemReader.getInstance().getSystemConfig().setString("filter", "lfs", "clean", "git-lfs clean -- %f");
SystemReader.getInstance().getSystemConfig().setString("filter", "lfs", "smudge", "git-lfs smudge -- %f");
SystemReader.getInstance().getSystemConfig().setString("filter", "lfs", "process", "git-lfs filter-process");
SystemReader.getInstance().getSystemConfig().setString("filter", "lfs", "required", "true");
} catch (Exception e) {
LOG.error("could not set git config; git lfs support may not work correctly", e);
}
FilterCommandRegistry.register(COMMAND_NAME_PATTERN, NoOpFilterCommand::new);
}

View File

@@ -39,8 +39,8 @@ import com.google.common.collect.ImmutableSet;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdSubclassMap;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.transport.BaseReceivePack;
import org.eclipse.jgit.transport.BaseReceivePack.PackParserListener;
import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.ReceivePack.PackParserListener;
import org.eclipse.jgit.transport.PackParser;
import org.slf4j.Logger;
@@ -76,7 +76,7 @@ public class CollectingPackParserListener implements PackParserListener
*
* @return listener
*/
public static CollectingPackParserListener get(BaseReceivePack pack)
public static CollectingPackParserListener get(ReceivePack pack)
{
PackParserListener listener = pack.getPackParserListener();
@@ -101,7 +101,7 @@ public class CollectingPackParserListener implements PackParserListener
*
* @param pack receive pack
*/
public static void set(BaseReceivePack pack)
public static void set(ReceivePack pack)
{
logger.trace("apply collecting listener to receive pack");
pack.setPackParserListener(new CollectingPackParserListener());

View File

@@ -5,8 +5,8 @@ import com.github.sdorra.shiro.SubjectAware;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.After;
import org.junit.Before;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -38,14 +38,14 @@ public class GitModifyCommand_LFSTest extends AbstractGitCommandTestBase {
private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class);
@Before
public void registerFilter() {
new GitLfsFilterContextListener(contextProvider).contextInitialized(null);
@BeforeClass
public static void registerFilter() {
new GitLfsFilterContextListener().contextInitialized(null);
}
@After
public void unregisterFilter() {
new GitLfsFilterContextListener(contextProvider).contextDestroyed(null);
@AfterClass
public static void unregisterFilter() {
new GitLfsFilterContextListener().contextDestroyed(null);
}
@Test

View File

@@ -57,7 +57,7 @@ public class HgConfigResource {
@GET
@Path("")
@Produces(HgVndMediaType.CONFIG)
@Operation(summary = "Hg configuration", description = "Returns the global mercurial configuration.", tags = "Mercurial")
@Operation(summary = "Hg configuration", description = "Returns the global mercurial configuration.", tags = "Mercurial", operationId = "hg_get_config")
@ApiResponse(
responseCode = "200",
description = "success",
@@ -97,7 +97,7 @@ public class HgConfigResource {
@PUT
@Path("")
@Consumes(HgVndMediaType.CONFIG)
@Operation(summary = "Modify hg configuration", description = "Modifies the global mercurial configuration.", tags = "Mercurial")
@Operation(summary = "Modify hg configuration", description = "Modifies the global mercurial configuration.", tags = "Mercurial", operationId = "hg_put_config")
@ApiResponse(
responseCode = "204",
description = "update success"

View File

@@ -48,7 +48,7 @@ public class SvnConfigResource {
@GET
@Path("")
@Produces(SvnVndMediaType.SVN_CONFIG)
@Operation(summary = "Svn configuration", description = "Returns the global subversion configuration.", tags = "Subversion")
@Operation(summary = "Svn configuration", description = "Returns the global subversion configuration.", tags = "Subversion", operationId = "svn_get_config")
@ApiResponse(
responseCode = "200",
description = "success",
@@ -88,7 +88,7 @@ public class SvnConfigResource {
@PUT
@Path("")
@Consumes(SvnVndMediaType.SVN_CONFIG)
@Operation(summary = "Modify svn configuration", description = "Modifies the global subversion configuration.", tags = "Subversion")
@Operation(summary = "Modify svn configuration", description = "Modifies the global subversion configuration.", tags = "Subversion", operationId = "svn_put_config")
@ApiResponse(
responseCode = "204",
description = "update success"

View File

@@ -185,7 +185,8 @@ public class BranchRootResource {
description = "create success",
headers = @Header(
name = "Location",
description = "uri to the created branch"
description = "uri to the created branch",
schema = @Schema(type = "string")
)
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")

View File

@@ -95,13 +95,14 @@ public class GroupCollectionResource {
@POST
@Path("")
@Consumes(VndMediaType.GROUP)
@Operation(summary = "Create group", description = "Creates a new group.", tags = "Group")
@Operation(summary = "Create group", description = "Creates a new group.", tags = "Group", operationId = "group_create")
@ApiResponse(
responseCode = "201",
description = "create success",
headers = @Header(
name = "Location",
description = "uri to the created group"
description = "uri to the created group",
schema = @Schema(type = "string")
)
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")

View File

@@ -12,9 +12,10 @@ import sonia.scm.util.ValidationUtil;
import javax.validation.constraints.Pattern;
import java.time.Instant;
import java.util.List;
import java.util.Map;
@Getter @Setter @NoArgsConstructor
@Getter
@Setter
@NoArgsConstructor
public class GroupDto extends HalRepresentation {
private Instant creationDate;
@@ -24,7 +25,6 @@ public class GroupDto extends HalRepresentation {
@Pattern(regexp = ValidationUtil.REGEX_NAME)
private String name;
private String type;
private Map<String, String> properties;
private List<String> members;
private boolean external;

View File

@@ -109,7 +109,8 @@ public class RepositoryCollectionResource {
description = "create success",
headers = @Header(
name = "Location",
description = "uri to the created repository"
description = "uri to the created repository",
schema = @Schema(type = "string")
)
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")

View File

@@ -7,16 +7,17 @@ import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import sonia.scm.util.ValidationUtil;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import java.time.Instant;
import java.util.List;
import java.util.Map;
@Getter @Setter @NoArgsConstructor
@Getter
@Setter
@NoArgsConstructor
public class RepositoryDto extends HalRepresentation {
@Email
@@ -30,10 +31,8 @@ public class RepositoryDto extends HalRepresentation {
private String namespace;
@Pattern(regexp = ValidationUtil.REGEX_REPOSITORYNAME)
private String name;
private boolean archived = false;
@NotEmpty
private String type;
protected Map<String, String> properties;
RepositoryDto(Links links, Embedded embedded) {
super(links, embedded);

View File

@@ -9,9 +9,9 @@ import lombok.extern.slf4j.Slf4j;
import sonia.scm.AlreadyExistsException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.web.VndMediaType;
@@ -71,7 +71,11 @@ public class RepositoryPermissionRootResource {
@ApiResponse(
responseCode = "201",
description = "creates",
headers = @Header(name = "Location", description = "uri of the created permission")
headers = @Header(
name = "Location",
description = "uri of the created permission",
schema = @Schema(type = "string")
)
)
@ApiResponse(
responseCode = "404",

View File

@@ -96,7 +96,8 @@ public class RepositoryRoleCollectionResource {
description = "create success",
headers = @Header(
name = "Location",
description = "uri to the created repository role"
description = "uri to the created repository role",
schema = @Schema(type = "string")
)
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")

View File

@@ -105,7 +105,8 @@ public class UserCollectionResource {
description = "create success",
headers = @Header(
name = "Location",
description = "uri to the created user"
description = "uri to the created user",
schema = @Schema(type = "string")
)
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")

View File

@@ -7,13 +7,12 @@ import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import sonia.scm.util.ValidationUtil;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import java.time.Instant;
import java.util.Map;
@NoArgsConstructor @Getter @Setter
public class UserDto extends HalRepresentation {
@@ -30,7 +29,6 @@ public class UserDto extends HalRepresentation {
@JsonInclude(JsonInclude.Include.NON_NULL)
private String password;
private String type;
private Map<String, String> properties;
UserDto(Links links, Embedded embedded) {
super(links, embedded);

View File

@@ -1,9 +1,9 @@
/**
* Copyright (c) 2010, Sebastian Sdorra All rights reserved.
*
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* <p>
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. 2. Redistributions in
* binary form must reproduce the above copyright notice, this list of
@@ -11,7 +11,7 @@
* materials provided with the distribution. 3. Neither the name of SCM-Manager;
* nor the names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -22,60 +22,59 @@
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* <p>
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.plugin;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.Lists;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
//~--- JDK imports ------------------------------------------------------------
/**
* {@link ClassLoader} which is able to load classes and resources from all
* plugins.
*
* @author Sebastian Sdorra
*/
public final class UberClassLoader extends ClassLoader
{
public final class UberClassLoader extends ClassLoader {
/**
* Constructs ...
*
*
* @param parent
* @param plugins
*/
public UberClassLoader(ClassLoader parent, Iterable<InstalledPlugin> plugins)
{
super(parent);
this.plugins = plugins;
private final Set<ClassLoader> pluginClassLoaders;
private final ConcurrentMap<String, WeakReference<Class<?>>> cache = Maps.newConcurrentMap();
public UberClassLoader(ClassLoader parent, Iterable<InstalledPlugin> plugins) {
this(parent, collectClassLoaders(plugins));
}
//~--- methods --------------------------------------------------------------
private static Set<ClassLoader> collectClassLoaders(Iterable<InstalledPlugin> plugins) {
ImmutableSet.Builder<ClassLoader> classLoaders = ImmutableSet.builder();
plugins.forEach(plugin -> classLoaders.add(plugin.getClassLoader()));
return classLoaders.build();
}
@VisibleForTesting
UberClassLoader(ClassLoader parent, Set<ClassLoader> pluginClassLoaders) {
super(parent);
this.pluginClassLoaders = pluginClassLoaders;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException
{
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> clazz = getFromCache(name);
if (clazz == null) {
@@ -87,8 +86,8 @@ public final class UberClassLoader extends ClassLoader
}
private Class<?> findClassInPlugins(String name) throws ClassNotFoundException {
for (InstalledPlugin plugin : plugins) {
Class<?> clazz = findClass(plugin.getClassLoader(), name);
for (ClassLoader pluginClassLoader : pluginClassLoaders) {
Class<?> clazz = findClass(pluginClassLoader, name);
if (clazz != null) {
return clazz;
}
@@ -106,27 +105,14 @@ public final class UberClassLoader extends ClassLoader
}
}
/**
* Method description
*
*
* @param name
*
* @return
*/
@Override
protected URL findResource(String name)
{
protected URL findResource(String name) {
URL url = null;
for (InstalledPlugin plugin : plugins)
{
ClassLoader cl = plugin.getClassLoader();
for (ClassLoader pluginClassLoader : pluginClassLoaders) {
url = pluginClassLoader.getResource(name);
url = cl.getResource(name);
if (url != null)
{
if (url != null) {
break;
}
}
@@ -134,52 +120,26 @@ public final class UberClassLoader extends ClassLoader
return url;
}
/**
* Method description
*
*
* @param name
*
* @return
*
* @throws IOException
*/
@Override
protected Enumeration<URL> findResources(String name) throws IOException
{
List<URL> urls = Lists.newArrayList();
@SuppressWarnings("squid:S2112")
protected Enumeration<URL> findResources(String name) throws IOException {
Set<URL> urls = new LinkedHashSet<>();
for (InstalledPlugin plugin : plugins)
{
ClassLoader cl = plugin.getClassLoader();
urls.addAll(Collections.list(cl.getResources(name)));
for (ClassLoader pluginClassLoader : pluginClassLoaders) {
urls.addAll(Collections.list(pluginClassLoader.getResources(name)));
}
return Collections.enumeration(urls);
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param name
*
* @return
*/
private Class<?> getFromCache(String name)
{
private Class<?> getFromCache(String name) {
Class<?> clazz = null;
WeakReference<Class<?>> ref = cache.get(name);
if (ref != null)
{
if (ref != null) {
clazz = ref.get();
if (clazz == null)
{
if (clazz == null) {
cache.remove(name);
}
}
@@ -187,12 +147,4 @@ public final class UberClassLoader extends ClassLoader
return clazz;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final ConcurrentMap<String, WeakReference<Class<?>>> cache =
Maps.newConcurrentMap();
/** Field description */
private final Iterable<InstalledPlugin> plugins;
}

View File

@@ -138,19 +138,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
assertTrue(response.getContentAsString().contains("\"name\":\"repo\""));
}
@Test
public void shouldMapProperties() throws URISyntaxException, UnsupportedEncodingException {
Repository repository = mockRepository("space", "repo");
repository.setProperty("testKey", "testValue");
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertTrue(response.getContentAsString().contains("\"testKey\":\"testValue\""));
}
@Test
public void shouldGetAll() throws URISyntaxException, UnsupportedEncodingException {
PageResult<Repository> singletonPageResult = createSingletonPageResult(mockRepository("space", "repo"));

View File

@@ -77,16 +77,6 @@ public class RepositoryToRepositoryDtoMapperTest {
assertEquals("none@example.com", dto.getContact());
}
@Test
public void shouldMapPropertiesProperty() {
Repository repository = createTestRepository();
repository.setProperty("testKey", "testValue");
RepositoryDto dto = mapper.map(repository);
assertEquals("testValue", dto.getProperties().get("testKey"));
}
@Test
@SubjectAware(username = "unpriv")
public void shouldCreateLinksForUnprivilegedUser() {

View File

@@ -0,0 +1,74 @@
package sonia.scm.plugin;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.Resources;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(TempDirectory.class)
class UberClassLoaderTest {
private final URLClassLoader parentClassLoader = new URLClassLoader(new URL[0]);
@Test
void shouldOnlyUseClassloaderOnce(@TempDirectory.TempDir Path tempDir) throws IOException {
ClassLoader mailClassLoader = createClassLoader(tempDir, "plugin.txt", "mail");
ClassLoader reviewClassLoader = createClassLoader(mailClassLoader, tempDir, "plugin.txt", "review");
UberClassLoader uberClassLoader = new UberClassLoader(parentClassLoader, ImmutableSet.of(mailClassLoader, reviewClassLoader));
List<URL> resources = Collections.list(uberClassLoader.findResources("plugin.txt"));
assertThat(resources).hasSize(2);
assertThat(toContent(resources)).containsOnly("mail", "review");
}
@Test
void shouldReturnResourceFromEachPluginClassLoader(@TempDirectory.TempDir Path tempDir) throws IOException {
ClassLoader mailClassLoader = createClassLoader(tempDir, "scm.txt", "mail");
ClassLoader reviewClassLoader = createClassLoader(tempDir, "scm.txt", "review");
UberClassLoader uberClassLoader = new UberClassLoader(parentClassLoader, ImmutableSet.of(mailClassLoader, reviewClassLoader));
List<URL> resources = Collections.list(uberClassLoader.findResources("scm.txt"));
assertThat(toContent(resources)).containsOnly("mail", "review");
}
@SuppressWarnings("UnstableApiUsage")
private List<String> toContent(Iterable<URL> resources) throws IOException {
List<String> content = new ArrayList<>();
for (URL resource : resources) {
content.add(Resources.toString(resource, StandardCharsets.UTF_8));
}
return content;
}
private ClassLoader createClassLoader(Path tempDir, String resource, String value) throws IOException {
return createClassLoader(Thread.currentThread().getContextClassLoader(), tempDir, resource, value);
}
private ClassLoader createClassLoader(ClassLoader parent, Path tempDir, String resource, String value) throws IOException {
Path directory = tempDir.resolve(UUID.randomUUID().toString());
Files.createDirectory(directory);
Files.write(directory.resolve(resource), value.getBytes(StandardCharsets.UTF_8));
return new URLClassLoader(new URL[]{
directory.toUri().toURL()
}, parent);
}
}