]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6366 add component to access Ruby runtime from rack
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Wed, 1 Apr 2015 15:03:37 +0000 (17:03 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 17 Apr 2015 13:14:59 +0000 (15:14 +0200)
server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
server/sonar-server/src/main/java/org/sonar/server/ruby/PlatformRackBridge.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/ruby/PlatformRubyBridge.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/ruby/RackBridge.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/ruby/RubyBridge.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/ruby/RubyDatabaseMigration.java [new file with mode: 0644]
server/sonar-server/src/main/resources/org/sonar/server/ruby/call_upgrade_and_start.rb [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/ruby/PlatformRackBridgeTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/ruby/PlatformRubyBridgeTest.java [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/ruby/database_version.rb [new file with mode: 0644]

index 72e911b8dd41c331818a7d7f33585b9df604ed72..4b284ba4ba62b96b42498caa62f2551468d0409f 100644 (file)
@@ -145,6 +145,8 @@ import org.sonar.server.permission.InternalPermissionTemplateService;
 import org.sonar.server.permission.PermissionFinder;
 import org.sonar.server.permission.ws.PermissionsWs;
 import org.sonar.server.platform.monitoring.*;
+import org.sonar.server.ruby.PlatformRackBridge;
+import org.sonar.server.ruby.PlatformRubyBridge;
 import org.sonar.server.platform.ws.*;
 import org.sonar.server.plugins.*;
 import org.sonar.server.plugins.ws.InstalledPluginsWsAction;
@@ -249,6 +251,9 @@ class ServerComponents {
       new TempFolderProvider(),
       System2.INSTANCE,
 
+      // rack bridges
+      PlatformRackBridge.class,
+
       // DB
       DbClient.class,
 
@@ -322,6 +327,9 @@ class ServerComponents {
       DefaultServerUpgradeStatus.class,
       DatabaseMigrator.class,
 
+      // depends on Ruby
+      PlatformRubyBridge.class,
+
       // plugins
       ServerPluginJarsInstaller.class,
       ServerPluginJarInstaller.class,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ruby/PlatformRackBridge.java b/server/sonar-server/src/main/java/org/sonar/server/ruby/PlatformRackBridge.java
new file mode 100644 (file)
index 0000000..9f7a56c
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.ruby;
+
+import org.jruby.Ruby;
+import org.jruby.rack.RackApplicationFactory;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextEvent;
+
+/**
+ * Implementation of {@link RackBridge} which get access to the Rack object through the {@link ServletContext}.
+ */
+public class PlatformRackBridge implements RackBridge {
+  public static final String RACK_FACTORY_ATTR_KEY = "rack.factory";
+  private final ServletContext servletContext;
+
+  public PlatformRackBridge(ServletContext servletContext) {
+    this.servletContext = servletContext;
+  }
+
+  /**
+   * From {@link org.jruby.rack.RackServletContextListener#contextInitialized(ServletContextEvent)} implementation, we
+   * know that the {@link RackApplicationFactory} is stored in the {@link ServletContext} under the attribute key
+   * {@link #RACK_FACTORY_ATTR_KEY}.
+   *
+   * @return a {@link Ruby} object representing the runtime environment of the RoR application of the platform.
+   *
+   * @throws RuntimeException if the {@link RackApplicationFactory} can not be retrieved
+   */
+  @Override
+  public Ruby getRubyRuntime() {
+    Object attribute = servletContext.getAttribute(RACK_FACTORY_ATTR_KEY);
+    if (!(attribute instanceof RackApplicationFactory)) {
+      throw new RuntimeException("Can not retrieve the RackApplicationFactory from ServletContext. " +
+        "Ruby runtime can not be retrieved");
+    }
+
+    RackApplicationFactory rackApplicationFactory = (RackApplicationFactory) attribute;
+    return rackApplicationFactory.getApplication().getRuntime();
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ruby/PlatformRubyBridge.java b/server/sonar-server/src/main/java/org/sonar/server/ruby/PlatformRubyBridge.java
new file mode 100644 (file)
index 0000000..9c97031
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.ruby;
+
+import org.jruby.Ruby;
+import org.jruby.RubyNil;
+import org.jruby.RubyRuntimeAdapter;
+import org.jruby.embed.InvokeFailedException;
+import org.jruby.javasupport.JavaEmbedUtils;
+import org.jruby.javasupport.JavaUtil;
+import org.jruby.runtime.builtin.IRubyObject;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class PlatformRubyBridge implements RubyBridge {
+  private static final String CALL_UPGRADE_AND_START_RB_FILENAME = "call_upgrade_and_start.rb";
+
+  private final RackBridge rackBridge;
+  private final RubyRuntimeAdapter adapter = JavaEmbedUtils.newRuntimeAdapter();
+
+  public PlatformRubyBridge(RackBridge rackBridge) {
+    this.rackBridge = rackBridge;
+  }
+
+  @Override
+  public RubyDatabaseMigration databaseMigration() {
+    final CallUpgradeAndStart callUpgradeAndStart = parseMethodScriptToInterface(
+      CALL_UPGRADE_AND_START_RB_FILENAME, CallUpgradeAndStart.class
+      );
+
+    return new RubyDatabaseMigration() {
+      @Override
+      public void trigger() {
+        callUpgradeAndStart.callUpgradeAndStart();
+      }
+    };
+  }
+
+  /**
+   * Parses a Ruby script that defines a single method and returns an instance of the specified interface type as a
+   * wrapper to this Ruby method.
+   */
+  private <T> T parseMethodScriptToInterface(String fileName, Class<T> clazz) {
+    try (InputStream in = getClass().getResourceAsStream(fileName)) {
+      Ruby rubyRuntime = rackBridge.getRubyRuntime();
+      JavaEmbedUtils.EvalUnit evalUnit = adapter.parse(rubyRuntime, in, fileName, 0);
+      IRubyObject rubyObject = evalUnit.run();
+      Object receiver = JavaEmbedUtils.rubyToJava(rubyObject);
+      T wrapper = getInstance(rubyRuntime, receiver, clazz);
+      return wrapper;
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to load script " + fileName, e);
+    }
+  }
+
+  /**
+   * Fork of method {@link org.jruby.embed.internal.EmbedRubyInterfaceAdapterImpl#getInstance(Object, Class)}
+   */
+  @SuppressWarnings("unchecked")
+  public <T> T getInstance(Ruby runtime, Object receiver, Class<T> clazz) {
+    if (clazz == null || !clazz.isInterface()) {
+      return null;
+    }
+    Object o;
+    if (receiver == null || receiver instanceof RubyNil) {
+      o = JavaEmbedUtils.rubyToJava(runtime, runtime.getTopSelf(), clazz);
+    } else if (receiver instanceof IRubyObject) {
+      o = JavaEmbedUtils.rubyToJava(runtime, (IRubyObject) receiver, clazz);
+    } else {
+      IRubyObject rubyReceiver = JavaUtil.convertJavaToRuby(runtime, receiver);
+      o = JavaEmbedUtils.rubyToJava(runtime, rubyReceiver, clazz);
+    }
+    String name = clazz.getName();
+    try {
+      Class<T> c = (Class<T>) Class.forName(name, true, o.getClass().getClassLoader());
+      return c.cast(o);
+    } catch (ClassNotFoundException e) {
+      throw new InvokeFailedException(e);
+    }
+  }
+
+  /**
+   * Interface which must be public to be used by the Ruby engine but that hides name of the Ruby method in the Ruby
+   * script from the rest of the platform (only {@link RubyDatabaseMigration} is known to the platform).
+   */
+  public interface CallUpgradeAndStart {
+
+    /**
+     * Java method that calls the upgrade_and_start method defined in the {@code call_upgrade_and_start.rb} script.
+     */
+    void callUpgradeAndStart();
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ruby/RackBridge.java b/server/sonar-server/src/main/java/org/sonar/server/ruby/RackBridge.java
new file mode 100644 (file)
index 0000000..745a9c1
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.ruby;
+
+import org.jruby.Ruby;
+
+/**
+ * Acts as a bridge between the Java application of the platform and the Rack application.
+ */
+public interface RackBridge {
+  /**
+   * Provides access to {@link Ruby} runtime instance created by the Rack application.
+   * @return
+   */
+  Ruby getRubyRuntime();
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ruby/RubyBridge.java b/server/sonar-server/src/main/java/org/sonar/server/ruby/RubyBridge.java
new file mode 100644 (file)
index 0000000..1861263
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.ruby;
+
+/**
+ * This component acts as a bridge between a Ruby runtime and Java. Each method it defines creates a wrapping
+ * Java object which an underlying Ruby implementation.
+ */
+public interface RubyBridge {
+  /**
+   * Returns a wrapper class that allows calling the database migration in Ruby.
+   *
+   * @return a  {@link RubyDatabaseMigration}
+   */
+  RubyDatabaseMigration databaseMigration();
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ruby/RubyDatabaseMigration.java b/server/sonar-server/src/main/java/org/sonar/server/ruby/RubyDatabaseMigration.java
new file mode 100644 (file)
index 0000000..9e311d3
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.ruby;
+
+/**
+ * Represents the database migration written in Ruby.
+ */
+public interface RubyDatabaseMigration {
+  /**
+   * Triggers the Ruby migration with ActiveRecord.
+   * <strong>This is not thread safe!</strong>
+   */
+  void trigger();
+}
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/ruby/call_upgrade_and_start.rb b/server/sonar-server/src/main/resources/org/sonar/server/ruby/call_upgrade_and_start.rb
new file mode 100644 (file)
index 0000000..5359ff1
--- /dev/null
@@ -0,0 +1,8 @@
+# this script defines a method which calls the class method "upgrade_and_start" of the DatabaseVersion class defined
+# in /server/sonar-web/src/main/webapp/WEB-INF/lib/database_version.rb
+
+require 'database_version'
+
+def call_upgrade_and_start
+  DatabaseVersion.upgrade_and_start
+end
\ No newline at end of file
diff --git a/server/sonar-server/src/test/java/org/sonar/server/ruby/PlatformRackBridgeTest.java b/server/sonar-server/src/test/java/org/sonar/server/ruby/PlatformRackBridgeTest.java
new file mode 100644 (file)
index 0000000..242f32c
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.ruby;
+
+import org.jruby.rack.RackApplication;
+import org.jruby.rack.RackApplicationFactory;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import javax.servlet.ServletContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class PlatformRackBridgeTest {
+  @Mock
+  private ServletContext servletContext;
+  @Mock
+  private RackApplicationFactory rackApplicationFactory;
+  @Mock
+  private RackApplication rackApplication;
+
+  @InjectMocks
+  PlatformRackBridge underTest;
+
+  @Before
+  public void setUp() throws Exception {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  @Test(expected = RuntimeException.class)
+  public void getRubyRuntime_throws_RE_when_RackApplicationFactory_is_not_in_ServletContext() throws Exception {
+    underTest.getRubyRuntime();
+  }
+
+  @Test
+  public void getRubyRuntime_returns_Ruby_instance_from_rack_application() throws Exception {
+    when(servletContext.getAttribute("rack.factory")).thenReturn(rackApplicationFactory);
+    when(rackApplicationFactory.getApplication()).thenReturn(rackApplication);
+
+    // since Ruby object can not be mocked and creating a Ruby instance is costly, we only make sure rackApplication#getRuntime method is
+    // called and that we get null (as rackApplication#getRuntime() returns null)
+    assertThat(underTest.getRubyRuntime()).isNull();
+    verify(rackApplication).getRuntime();
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/ruby/PlatformRubyBridgeTest.java b/server/sonar-server/src/test/java/org/sonar/server/ruby/PlatformRubyBridgeTest.java
new file mode 100644 (file)
index 0000000..527f48f
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.ruby;
+
+import com.google.common.collect.ImmutableList;
+import org.jruby.embed.LocalContextScope;
+import org.jruby.embed.ScriptingContainer;
+import org.jruby.exceptions.RaiseException;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class PlatformRubyBridgeTest {
+  private static ScriptingContainer container = setupScriptingContainer();
+
+  private RackBridge rackBridge = mock(RackBridge.class);
+  private PlatformRubyBridge underTest;
+
+  @Before
+  public void setUp() throws Exception {
+    when(rackBridge.getRubyRuntime()).thenReturn(container.getProvider().getRuntime());
+    underTest = new PlatformRubyBridge(rackBridge);
+  }
+
+  /**
+   * Creates a Ruby runtime which loading path includes the test resource directory where our Ruby test DatabaseVersion
+   * is defined.
+   */
+  private static ScriptingContainer setupScriptingContainer() {
+    try {
+      ScriptingContainer container = new ScriptingContainer(LocalContextScope.CONCURRENT);
+      URL resource = PlatformRubyBridge.class.getResource("database_version.rb");
+      String dirPath = new File(resource.toURI()).getParentFile().getPath();
+      container.setLoadPaths(ImmutableList.of(dirPath));
+
+      return container;
+    } catch (URISyntaxException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * unit test only makes sure the wrapping and method forwarding provided by JRuby works so building the
+   * RubyDatabaseMigration object and calling its trigger method is enough as it would otherwise raise an exception
+   */
+  @Test
+  public void testDatabaseMigration() {
+    try {
+      underTest.databaseMigration().trigger();
+    } catch (RaiseException e) {
+      throw new RuntimeException("Loading error with container loadPath " + container.getLoadPaths(), e);
+    }
+  }
+
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/ruby/database_version.rb b/server/sonar-server/src/test/resources/org/sonar/server/ruby/database_version.rb
new file mode 100644 (file)
index 0000000..e1cc2ca
--- /dev/null
@@ -0,0 +1,10 @@
+# a dummy class DatabaseVersion that "mocks" the DatabaseVersion class of the platform by defining a class method called upgrade_and_start
+# unit test only makes sure the wrapping and method forwarding provided by JRuby works so providing an empty method is enough as
+# it would otherwise raise an exception
+class DatabaseVersion
+
+  def self.upgrade_and_start
+
+  end
+
+end