# 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;
      }
    }