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

org.glassfish.hk2.classmodel.reflect.Parser Maven / Gradle / Ivy

There is a newer version: 4.0.0-M3
Show newest version
/*
 * Copyright (c) 2023 Contributors to the Eclipse Foundation
 * Copyright (c) 2010, 2024 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.hk2.classmodel.reflect;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EmptyStackException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.glassfish.hk2.classmodel.reflect.impl.TypeProxy;
import org.glassfish.hk2.classmodel.reflect.impl.TypesCtr;
import org.glassfish.hk2.classmodel.reflect.util.DirectoryArchive;
import org.glassfish.hk2.classmodel.reflect.util.JarArchive;
import org.glassfish.hk2.classmodel.reflect.util.ResourceLocator;
import org.objectweb.asm.ClassReader;

/**
 * Parse jar files or directories and create the model for any classes found.
 *
 * @author Jerome Dochez
 */
public class Parser implements Closeable {

    public static final String DEFAULT_WAIT_SYSPROP = "hk2.parser.timeout";

    private final ParsingContext context;
    private final Map processedURI = Collections.synchronizedMap(new HashMap());

    private final ReentrantLock thislock = new ReentrantLock();
    private final ReentrantLock futuresLock = new ReentrantLock();
    private final Stack> futures = new Stack<>();
    private final ExecutorService executorService;
    private final boolean ownES;

    // used to safeguard between await and parse
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    private final int DEFAULT_TIMEOUT = Integer.getInteger(DEFAULT_WAIT_SYSPROP, 100);


    public Parser(ParsingContext context) {
        this.context = context;
        executorService = (context.executorService==null?createExecutorService():context.executorService);
        ownES = context.executorService==null;
    }

    public Exception[] awaitTermination() throws InterruptedException {
        return awaitTermination(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
    }

    public Exception[] awaitTermination(int timeOut, TimeUnit unit) throws InterruptedException {
        List exceptions = new ArrayList<>();
        final Logger logger = context.logger;
        while(futures.size()>0) {
            if (context.logger.isLoggable(Level.FINE)) {
                 context.logger.log(Level.FINE, "Await iterating at " + System.currentTimeMillis() + " waiting for " + futures.size());
            }
            Future f;
            futuresLock.lock();
                try {
                    f = futures.pop();
                } catch(EmptyStackException e) {
                    // it's ok, another thread took the load from us.
                    f = null;
            } finally {
                futuresLock.unlock();
            }
            if (f!=null) {
                try {
                    Result result = f.get(timeOut, unit);
                    if (context.logger.isLoggable(Level.FINE)) {
                        context.logger.log(Level.FINE, "future finished at " + System.currentTimeMillis() + " for " + result.name);
                    }
                    context.logger.log(Level.FINER, "result {0}", result);
                    if (result!=null && result.fault!=null) {
                       context.logger.log(Level.WARNING, "result fault {0}", result);
                    }
                    if (result!=null && result.fault!=null) {
                       exceptions.add(result.fault);
                    }
                } catch (TimeoutException e) {
                    exceptions.add(e);
                } catch (ExecutionException e) {
                    exceptions.add(e);
                }
            }
        }

        lock.writeLock().lock();
        try {

            // now we need to visit all the types that got referenced but not visited
            final ResourceLocator locator = context.getLocator();
            if (locator!=null) {
                context.logger.fine("Visiting unvisited references ...");
                context.types.onNotVisitedEntries(new TypesCtr.ProxyTask() {
                    @Override
                    public void on(TypeProxy proxy) {

                        String name = proxy.getName();
                        // make this name a resource name...
                        String resourceName = name.replaceAll("\\.", "/") + ".class";

                        InputStream is = null;
                        try {
                            is = locator.openResourceStream(resourceName);
                            if (null == is) {
                              return;
                            }

                            // now visit...
                            logger.log(Level.FINE, "Going to visit {0}", resourceName);
                            ClassReader cr = new ClassReader(is);
                            try {
                                URL url = locator.getResource(resourceName);
                                File file = getFilePath(url.getPath(), resourceName);
                                URI definingURI = getDefiningURI(file);
                                if (logger.isLoggable(Level.FINE)) {
                                    logger.fine("Visiting file=" + file + "; definingURI=" + definingURI);
                                }
                                cr.accept(context.getClassVisitor(definingURI, resourceName), ClassReader.SKIP_DEBUG);
                            } catch (Throwable e) {
                                logger.log(Level.SEVERE, "Exception while visiting {0}", name);
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        } finally {
                            if (is != null) {
                                try {
                                    is.close();
                                } catch (IOException e) {
                                    logger.log(Level.SEVERE, "Exception while closing " + resourceName + " stream", e);
                                }
                            }
                        }
                    }
                });
            } else {
                context.types.clearNonVisitedEntries();
            }
            close();
            return exceptions.toArray(new Exception[exceptions.size()]);
        } finally {
            lock.writeLock().unlock();
        }
    }

    private static URI getDefiningURI(File file) {
      try {
        return file.getCanonicalFile().toURI();
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }

    private static File getFilePath(String path, String resourceName) {
      path = path.substring(0, path.length() - resourceName.length());
      if (path.endsWith("!/")) {
        path = path.substring(0, path.length()-2);
      }
      if (path.startsWith("file:/")) {
        path = path.substring(6, path.length());
      }
      File file = new File(path);
      return file;
    }

    @Override
    public void close() {
      // if we own the executor service, time to shut it down.
      if (executorService!=null && ownES) {
          executorService.shutdown();
      }
    }

    public void parse(final File source, final Runnable doneHook) throws IOException {
        // todo : use protocol to lookup implementation
        final ArchiveAdapter adapter = createArchiveAdapter(source, doneHook);

        final Runnable cleanUpAndNotify = new Runnable() {
          @Override
          public void run() {
            try {
              try {
                adapter.close();
              } catch (IOException e) {
                throw new RuntimeException(e);
              }
            } finally {
              if (doneHook!=null) {
                  doneHook.run();
              }
            }
          }
        };
        parse(adapter, cleanUpAndNotify);
    }

    private ArchiveAdapter createArchiveAdapter(File source, Runnable doneHook)
            throws IOException {
      try {
        ArchiveAdapter aa = source.isFile()?new JarArchive(this, source.toURI()):
                                            new DirectoryArchive(this, source);
        return aa;
      } catch (IOException e) {
        if (doneHook!=null) {
          doneHook.run();
        }
        throw e;
      }
    }

    /**
     * Parse the archive adapter entries and run the runnable hook on completion.
     *
     * @param source the archive adapter to parse
     * @param doneHook the runnable hook to run after completion
     * @return the future object to monitor the result of the parsing.
     *
     * @throws IOException thrown by the source archive adapter when accessing entries
     */
    public Future parse(final ArchiveAdapter source, final Runnable doneHook) throws IOException {

        lock.readLock().lock();
        try {

            ExecutorService es = executorService;
            boolean immediateShutdown = false;
            if (es.isShutdown()) {
                context.logger.info("Executor service is shutdown, since awaitTermination was called, " +
                        "provide an executor service instance from the context to avoid automatic shutdown");
                es = createExecutorService();
                immediateShutdown = true;
            }
            final Logger logger = context.logger;
            Types types = getResult(source.getURI());
            if (types!=null) {
                if (logger.isLoggable(Level.FINE)) {
                    logger.fine("Skipping reparsing..." + source.getURI());
                }
                doneHook.run();
                return null;
            }
            if (logger.isLoggable(Level.FINE)) {
                logger.log(Level.FINE, "at " + System.currentTimeMillis() + "in " + this + " submitting file " + source.getURI().getPath());
                logger.log(Level.FINE, "submitting file " + source.getURI().getPath());
            }
            Future future = es.submit(new Callable() {
                @Override
                public Result call() throws Exception {
                    try {
                        if (logger.isLoggable(Level.FINE)) {
                            logger.log(Level.FINE, "elected file " + source.getURI().getPath());
                        }
                        if (logger.isLoggable(Level.FINE)) {
                            context.logger.log(Level.FINE, "started working at " + System.currentTimeMillis() + "in "
                                    + this + " on " + source.getURI().getPath());
                        }
                        doJob(source, doneHook);

                        return new Result(source.getURI().getPath(), null);
                    } catch (Exception e) {
                        logger.log(Level.SEVERE, "Exception while parsing file " + source.getURI(), e);
                        return new Result(source.getURI().getPath(), e);
                    }
                }
            });
            futuresLock.lock();
            try {
                futures.add(future);
            } finally {
                futuresLock.unlock();
            }
            if (immediateShutdown) {
                es.shutdown();
            }
            return future;
        } finally {
            lock.readLock().unlock();
        }
    }

    private Types getResult(URI uri) {
        thislock.lock();
        try {
            return processedURI.get(uri.getSchemeSpecificPart());
        } finally {
            thislock.unlock();
        }
    }

    private void saveResult(URI uri, Types types) {
        thislock.lock();
        try {
            this.processedURI.put(uri.getPath(), types);
        } finally {
            thislock.unlock();
        }
    }

    private void doJob(final ArchiveAdapter adapter, final Runnable doneHook) throws Exception {
        final Logger logger = context.logger;
        long startTime = System.currentTimeMillis();
        if (logger.isLoggable(Level.FINE)) {
            logger.fine("Parsing " + adapter.getURI() + " on thread " + Thread.currentThread().getName());
        }
        if (context.archiveSelector == null || context.archiveSelector.selects(adapter)) {
            final URI uri = adapter.getURI();
            if (logger.isLoggable(Level.FINE)) {
                logger.log(Level.FINE, "Parsing file {0}", uri.getPath());
            }

            adapter.onSelectedEntries(
                    new ArchiveAdapter.Selector() {
                        @Override
                        public boolean isSelected(ArchiveAdapter.Entry entry) {
                            return entry.name.endsWith(".class");
                        }
                    },
                    new ArchiveAdapter.EntryTask() {
                        @Override
                        public void on(ArchiveAdapter.Entry entry, InputStream is) throws IOException {
                            if (logger.isLoggable(Level.FINER)) {
                                logger.log(Level.FINER, "Parsing class " + entry.name);
                            }
                            try {
                                ClassReader cr = new ClassReader(is);
                                cr.accept(context.getClassVisitor(uri, entry.name, true), ClassReader.SKIP_DEBUG);
                            } catch (Throwable e) {
                                logger.log(Level.SEVERE, "Exception while visiting " + entry.name
                                        + " of size " + entry.size, e);
                            }
                        }
                    },
                    logger
            );
            saveResult(uri, context.getTypes());
        }
        if (logger.isLoggable(Level.FINE)) {
            logger.log(Level.FINE,"Finished parsing " + adapter.getURI().getPath() + " at " + System.currentTimeMillis() + " in "
                + (System.currentTimeMillis() - startTime) + " ms");
        }
        if (logger.isLoggable(Level.FINE)) {
            logger.log(Level.FINE, "before running doneHook" + adapter.getURI().getPath());
        }
        if (doneHook != null) {
            doneHook.run();
        }
        if (logger.isLoggable(Level.FINE)) {
            logger.log(Level.FINE, "after running doneHook " + adapter.getURI().getPath());
        }
    }

    /**
     * Returns the context this parser instance was initialized with during
     * the call to {@link Parser#Parser(ParsingContext)}
     *
     * @return the parsing context this parser uses to store the parsing
     * activities results.
     */
    public ParsingContext getContext() {
        return context;
    }

    private ExecutorService createExecutorService() {
        // TODO: may have to dumb this down since there appears to be issues in ZipFile processing when there is contention on the same file or directory
        // FIXME: anything other than 1 is not stable since the class-model framework is not thread safe (visitors are shared)
//        Runtime runtime = Runtime.getRuntime();
//        int nbOfProcessors = runtime.availableProcessors();
        int nbOfProcessors = 1;

        return Executors.newFixedThreadPool(nbOfProcessors, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("Hk2-jar-scanner-" + t.getId());
                t.setDaemon(true);
                return t;
            }
        });
    }


    public static class Result {
        public final String name;
        public  final Exception fault;

        private Result(String name, Exception fault) {
            this.name = name;
            this.fault = fault;
        }

        @Override
        public String toString() {
            return super.toString() + " Result for " + name;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy