EquinoxHackUtilImpl.java

/*
 * Copyright (C) 2011 Everit Kft. (http://www.everit.biz)
 *
 * Licensed 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.everit.persistence.lqmg.internal;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;

import org.eclipse.osgi.baseadaptor.BaseAdaptor;
import org.eclipse.osgi.baseadaptor.BaseData;
import org.eclipse.osgi.baseadaptor.bundlefile.BundleFile;
import org.eclipse.osgi.framework.internal.core.AbstractBundle;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.osgi.service.resolver.BundleSpecification;
import org.eclipse.osgi.service.resolver.ImportPackageSpecification;
import org.eclipse.osgi.service.resolver.PlatformAdmin;
import org.eclipse.osgi.service.resolver.State;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.wiring.BundleCapability;
import org.osgi.framework.wiring.BundleRequirement;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.framework.wiring.BundleWiring;

/**
 * Equinox specific implementation of the {@link HackUtil}.
 */
public class EquinoxHackUtilImpl implements HackUtil {

  private static final int BUFFER_SIZE = 1024;

  private static final Logger LOGGER = Logger.getLogger(EquinoxHackUtilImpl.class.getName());

  private <V> String convertClauseFieldsToString(final Map<String, V> map,
      final boolean directives) {
    if (map.size() == 0) {
      return "";
    }
    String assignment = directives ? ":=" : "=";
    Set<Entry<String, V>> set = map.entrySet();
    StringBuilder sb = new StringBuilder();
    for (Entry<String, V> entry : set) {
      sb.append(";");
      String key = entry.getKey();
      Object value = entry.getValue();
      if (value instanceof List) {
        @SuppressWarnings("unchecked")
        List<Object> list = (List<Object>) value;
        if (list.size() == 0) {
          continue;
        }
        sb.append(key).append(assignment).append('"');
        for (Object object : list) {
          sb.append(escapeClauseValue(object)).append(',');
        }
        sb.setLength(sb.length() - 1);
        sb.append('"');
      } else {
        sb.append(key).append(assignment).append('"').append(escapeClauseValue(value)).append('"');
      }
    }
    return sb.toString();
  }

  private void copyBundleEntryIntoJar(final Bundle bundle, final String entry,
      final JarOutputStream jarOut)
          throws IOException {
    jarOut.putNextEntry(new ZipEntry(entry));
    URL resource = bundle.getResource(entry);
    InputStream in = resource.openStream();
    try {
      byte[] buf = new byte[BUFFER_SIZE];
      int r = in.read(buf);
      while (r > -1) {
        jarOut.write(buf, 0, r);
        r = in.read(buf);
      }
    } finally {
      in.close();
    }
  }

  private String createClauseString(final String namespace, final Map<String, Object> attributeMap,
      final Map<String, String> directiveMap) {
    String attributesPart = convertClauseFieldsToString(attributeMap, false);
    String directivesPart = convertClauseFieldsToString(directiveMap, true);
    StringBuilder sb = new StringBuilder(namespace);
    if (!"".equals(attributesPart)) {
      sb.append(attributesPart);
    }
    if (!"".equals(directivesPart)) {
      sb.append(directivesPart);
    }
    return sb.toString();
  }

  private Manifest createHackedManifest(final Bundle bundle,
      final BundleDescription bundleDescription,
      final List<BundleCapability> availableCapabilities) {
    List<BundleRequirement> declaredRequirements = bundleDescription.getDeclaredRequirements(null);

    Manifest manifest = readOriginalManifest(bundle);
    Attributes mainAttributes = manifest.getMainAttributes();

    hackImportPackageManifestHeader(bundleDescription, availableCapabilities, mainAttributes);
    hackRequireBundleManifestHeader(bundleDescription, availableCapabilities, mainAttributes);

    StringBuilder sb = new StringBuilder();
    for (BundleRequirement declaredRequirement : declaredRequirements) {
      String namespace = declaredRequirement.getNamespace();
      if (namespace.equals(BundleRevision.PACKAGE_NAMESPACE)
          || namespace.equals(BundleRevision.HOST_NAMESPACE)
          || namespace.equals(BundleRevision.BUNDLE_NAMESPACE)) {
        continue;
      }
      Map<String, String> directives = declaredRequirement.getDirectives();
      Map<String, Object> attributes = declaredRequirement.getAttributes();

      boolean optional = Constants.RESOLUTION_OPTIONAL.equals(declaredRequirement
          .getDirectives().get(Constants.RESOLUTION_DIRECTIVE));
      if (!optional && !requirementSatisfiable(declaredRequirement, availableCapabilities)) {
        LOGGER.info(
            "[HACK]: Making Require-Capability optional in bundle " + bundleDescription.toString()
                + ": " + declaredRequirement.toString());
        directives = new HashMap<String, String>(directives);
        directives.put(Constants.RESOLUTION_DIRECTIVE, Constants.RESOLUTION_OPTIONAL);
      }
      String clauseString = createClauseString(namespace, attributes, directives);
      if (sb.length() > 0) {
        sb.append(",");
      }
      sb.append(clauseString);
    }
    if (sb.length() > 0) {
      mainAttributes.putValue(Constants.REQUIRE_CAPABILITY, sb.toString());
    }
    return manifest;
  }

  private String escapeClauseValue(final Object object) {
    String stringValue = String.valueOf(object);
    return stringValue.replace("\\", "\\\\").replace("\"", "\\\"");
  }

  private List<BundleCapability> getAllCapabilities(final Bundle[] bundles, final State state) {
    List<BundleCapability> availableCapabilities = new ArrayList<BundleCapability>();
    for (Bundle bundle : bundles) {
      BundleDescription bundleDescription = state.getBundle(bundle.getBundleId());
      List<BundleCapability> declaredCapabilities = bundleDescription.getDeclaredCapabilities(null);
      availableCapabilities.addAll(declaredCapabilities);
    }
    return availableCapabilities;
  }

  private void hackBundle(final Bundle bundle, final BundleDescription bundleDescription,
      final List<BundleCapability> availableCapabilities) {
    Manifest manifest = createHackedManifest(bundle, bundleDescription, availableCapabilities);

    ByteArrayOutputStream bout = new ByteArrayOutputStream();
    try {
      JarOutputStream jarOut = new JarOutputStream(bout, manifest);
      AbstractBundle abstractBundle = (AbstractBundle) bundle;
      BaseData bundleData = (BaseData) abstractBundle.getBundleData();
      BundleFile bundleFile = bundleData.getBundleFile();
      BaseAdaptor adaptor = (BaseAdaptor) abstractBundle.getFramework().getAdaptor();

      List<String> entries =
          adaptor.listEntryPaths(Arrays.asList(new BundleFile[] { bundleFile }), "/", null,
              BundleWiring.FINDENTRIES_RECURSE);

      for (String entry : entries) {
        if (!"META-INF/MANIFEST.MF".equals(entry) && !entry.endsWith("/")) {
          copyBundleEntryIntoJar(bundle, entry, jarOut);
        }
      }
      jarOut.close();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    try {
      bundle.update(new ByteArrayInputStream(bout.toByteArray()));
    } catch (BundleException e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public void hackBundles(final Framework osgiContainer, final File tempDirectory) {
    BundleContext systemBundleContext = osgiContainer.getBundleContext();

    ServiceReference<PlatformAdmin> platformServiceSR = systemBundleContext
        .getServiceReference(PlatformAdmin.class);

    PlatformAdmin platformAdmin = systemBundleContext.getService(platformServiceSR);
    State state = platformAdmin.getState();

    Bundle[] bundles = systemBundleContext.getBundles();
    List<BundleCapability> availableCapabilities = getAllCapabilities(bundles, state);
    for (Bundle bundle : bundles) {
      if (bundle.getState() == Bundle.INSTALLED) {
        BundleDescription bundleDescription = state.getBundle(bundle.getBundleId());
        hackBundle(bundle, bundleDescription, availableCapabilities);
      }
    }
  }

  private void hackImportPackageManifestHeader(final BundleDescription bundleDescription,
      final List<BundleCapability> availableCapabilities, final Attributes mainAttributes) {

    StringBuilder hackedImportPackageSB = new StringBuilder();

    ImportPackageSpecification[] allImports = bundleDescription.getImportPackages();

    for (ImportPackageSpecification importPackage : allImports) {
      if (hackedImportPackageSB.length() > 0) {
        hackedImportPackageSB.append(",");
      }
      hackedImportPackageSB.append(importPackage.getName()).append(";version=\"")
          .append(importPackage.getVersionRange()).append("\"");

      boolean optional = Constants.RESOLUTION_OPTIONAL.equals(importPackage
          .getDirective(Constants.RESOLUTION_DIRECTIVE));
      if (optional
          || !requirementSatisfiable(importPackage.getRequirement(), availableCapabilities)) {
        if (!optional) {
          LOGGER.info(
              "[HACK]: Making Import-Package optional in bundle " + bundleDescription.toString()
                  + ": " + hackedImportPackageSB.toString());
        }
        hackedImportPackageSB.append(";").append(Constants.RESOLUTION_DIRECTIVE).append(":=")
            .append("\"")
            .append(Constants.RESOLUTION_OPTIONAL).append("\"");
      }

    }

    if (hackedImportPackageSB.length() > 0) {
      mainAttributes.putValue(Constants.IMPORT_PACKAGE, hackedImportPackageSB.toString());
    }
  }

  private void hackRequireBundleManifestHeader(final BundleDescription bundleDescription,
      final List<BundleCapability> availableCapabilities, final Attributes mainAttributes) {

    StringBuilder hackedRequireBundleSB = new StringBuilder();

    BundleSpecification[] requiredBundles = bundleDescription.getRequiredBundles();

    for (BundleSpecification requiredBundle : requiredBundles) {
      if (hackedRequireBundleSB.length() > 0) {
        hackedRequireBundleSB.append(",");
      }
      hackedRequireBundleSB.append(requiredBundle.getName() + ";bundle-version=\""
          + requiredBundle.getVersionRange() + "\"");

      if (requiredBundle.isOptional()
          || !requirementSatisfiable(requiredBundle.getRequirement(), availableCapabilities)) {
        if (!requiredBundle.isOptional()) {
          LOGGER.info(
              "[HACK]: Making Require-Bundle optional in bundle " + bundleDescription.toString()
                  + ": " + hackedRequireBundleSB.toString());
        }
        hackedRequireBundleSB.append(";").append(Constants.RESOLUTION_DIRECTIVE).append(":=")
            .append("\"")
            .append(Constants.RESOLUTION_OPTIONAL).append("\"");
      }
    }

    if (hackedRequireBundleSB.length() > 0) {
      mainAttributes.putValue(Constants.REQUIRE_BUNDLE, hackedRequireBundleSB.toString());
    }
  }

  private Manifest readOriginalManifest(final Bundle bundle) {
    URL manifestURL = bundle.getResource("/META-INF/MANIFEST.MF");
    InputStream manifestStream = null;
    try {
      manifestStream = manifestURL.openStream();
      return new Manifest(manifestStream);
    } catch (IOException e) {
      throw new RuntimeException(e);
    } finally {
      if (manifestStream != null) {
        try {
          manifestStream.close();
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
      }
    }
  }

  private boolean requirementSatisfiable(final BundleRequirement requirement,
      final List<BundleCapability> availableCapabilities) {
    for (BundleCapability bundleCapability : availableCapabilities) {
      try {
        if (requirement.matches(bundleCapability)) {
          return true;
        }
      } catch (RuntimeException e) {
        LOGGER.log(Level.WARNING, "Capability does not match the requirement.", e);
      }
    }
    return false;
  }
}