All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dellroad.jct.jshell.MemoryClassLoader Maven / Gradle / Ivy


/*
 * Copyright (C) 2023 Archie L. Cobbs. All rights reserved.
 */

package org.dellroad.jct.jshell;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.security.CodeSource;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * An {@link URLClassLoader} that maintains an updatable cache of class files (as {@code byte[]} arrays)
 * in memory from which classes may be loaded.
 *
 * 

* Classes found by this loader are assigned {@link URL}s that look like {@code memory:/com.example.MyClass}. */ public class MemoryClassLoader extends URLClassLoader { public static final String MEMORY_URL_SCHEME = "memory"; private final Map classDataMap = new HashMap<>(); // Constructor /** * Default constructor. * *

* Uses the current thread's context loader as the parent loader. */ public MemoryClassLoader() { this(Thread.currentThread().getContextClassLoader()); } /** * Constructor. * * @param parent parent class loader, possibly null */ public MemoryClassLoader(ClassLoader parent) { super(new URL[0], parent); } // Public Methods /** * Add a class to this loader, making it available for class resolution. * *

* If an existing class already exists under {@code className}, it will be replaced. * *

* If {@code classbytes} is null, any existing class will be removed. * * @param className Java class name * @param classbytes Java class file data * @throws IllegalArgumentException if either parameter is null */ public void putClass(String className, byte[] classbytes) { if (className == null) throw new IllegalArgumentException("null className"); synchronized (this) { if (classbytes != null) this.classDataMap.put(className, new ClassData(classbytes)); else this.classDataMap.remove(className); } } /** * Get the class from this loader that was previously added via {@link #putClass putClass()}, if any. * * @param className Java class name * @return previously added class, or null if none exists */ public synchronized ClassData getClass(String className) { return this.classDataMap.get(className); } // URLClassLoader // Make this public @Override public void addURL(URL url) { super.addURL(url); } @Override protected Class findClass(String className) throws ClassNotFoundException { final ClassData classData; synchronized (this) { classData = this.classDataMap.get(className); } if (classData == null) return super.findClass(className); final byte[] classbytes = classData.getClassbytes(); return this.defineClass(className, classbytes, 0, classbytes.length, (CodeSource)null); } @Override public URL findResource(String resourceName) { final URL resource = this.findClassDataResource(resourceName); if (resource != null) return resource; return super.findResource(resourceName); } @Override public Enumeration findResources(String resourceName) throws IOException { Enumeration resources = super.findResources(resourceName); final URL classDataResource = this.findClassDataResource(resourceName); if (classDataResource != null) { final ArrayList list = new ArrayList<>(); while (resources.hasMoreElements()) list.add(resources.nextElement()); list.add(classDataResource); resources = Collections.enumeration(list); } return resources; } // Locate the class file having the given resource filename private URL findClassDataResource(String resourceName) { // Validate if (resourceName == null) throw new IllegalArgumentException("null resourceName"); // Get the class name that would correspond to the given resource if (!resourceName.endsWith(".class") || resourceName.startsWith("/")) return null; final String className = resourceName.substring(0, resourceName.length() - 6).replace('/', '.'); // Do we have data for that class? final ClassData classData; synchronized (this) { classData = this.classDataMap.get(className); } if (classData == null) return null; // Build an URL for it try { final URI memoryURI = new URI(MEMORY_URL_SCHEME, null, "/" + className, null); return new URL(null, memoryURI.toString(), new MemoryURLStreamHandler(classData)); } catch (URISyntaxException | MalformedURLException e) { throw new RuntimeException("internal error", e); } } // ClassData // Holds a class definable by this loader private static class ClassData { private final byte[] classbytes; private final long createTime = System.currentTimeMillis(); ClassData(byte[] classbytes) { if (classbytes == null) throw new IllegalArgumentException("null classbytes"); this.classbytes = classbytes; } public byte[] getClassbytes() { return this.classbytes; } public long getCreateTime() { return this.createTime; } } // MemoryURLStreamHandler private static class MemoryURLStreamHandler extends URLStreamHandler { private final ClassData classData; MemoryURLStreamHandler(ClassData classData) { if (classData == null) throw new IllegalArgumentException("null classData"); this.classData = classData; } @Override protected URLConnection openConnection(URL url) { return new MemoryURLConnection(url, this.classData.getClassbytes(), this.classData.getCreateTime()); } } // MemoryURLConnection private static class MemoryURLConnection extends URLConnection { private static final String CONTENT_LENGTH_HEADER = "Content-Length"; private static final String DATE_HEADER = "Date"; private static final String LAST_MODIFIED_HEADER = "Last-Modified"; private final InputStream in; private final String[][] fields; // Constructors MemoryURLConnection(URL url, ClassData classData) { this(url, classData.getClassbytes(), classData.getCreateTime()); } MemoryURLConnection(URL url, byte[] classbytes, long timestamp) { super(url); if (classbytes == null) throw new IllegalArgumentException("null classbytes"); this.in = new ByteArrayInputStream(classbytes); final Instant instant = Instant.ofEpochMilli(timestamp); final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of("GMT")); final String timestampString = DateTimeFormatter.RFC_1123_DATE_TIME.format(zonedDateTime); this.fields = new String[][] { { CONTENT_LENGTH_HEADER, "" + classbytes.length }, { DATE_HEADER, timestampString }, { LAST_MODIFIED_HEADER, timestampString } }; } // URLConnection @Override public void connect() { // nothing to do } @Override public InputStream getInputStream() { return this.in; } @Override public String getHeaderField(String name) { return Stream.of(this.fields) .filter(pair -> pair[0].equals(name)) .map(pair -> pair[1]) .findFirst() .orElse(null); } @Override public Map> getHeaderFields() { return Stream.of(this.fields) .collect(Collectors.toMap(pair -> pair[0], pair -> Collections.singletonList(pair[1]))); } @Override public String getHeaderFieldKey(int index) { return index < this.fields.length ? this.fields[index][0] : null; } @Override public String getHeaderField(int index) { return index < this.fields.length ? this.fields[index][1] : null; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy