From: Sébastien Lesaint Date: Wed, 1 Apr 2015 15:03:37 +0000 (+0200) Subject: SONAR-6366 add component to access Ruby runtime from rack X-Git-Tag: 5.2-RC1~2232 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=56f97c01d574701e04b1eff64e4b4f35e4311319;p=sonarqube.git SONAR-6366 add component to access Ruby runtime from rack --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java index 72e911b8dd4..4b284ba4ba6 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java @@ -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 index 00000000000..9f7a56c550e --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/ruby/PlatformRackBridge.java @@ -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 index 00000000000..9c970319148 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/ruby/PlatformRubyBridge.java @@ -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 parseMethodScriptToInterface(String fileName, Class 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 getInstance(Ruby runtime, Object receiver, Class 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 c = (Class) 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 index 00000000000..745a9c146f2 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/ruby/RackBridge.java @@ -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 index 00000000000..1861263d516 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/ruby/RubyBridge.java @@ -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 index 00000000000..9e311d3b8f8 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/ruby/RubyDatabaseMigration.java @@ -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. + * This is not thread safe! + */ + 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 index 00000000000..5359ff11db7 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/ruby/call_upgrade_and_start.rb @@ -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 index 00000000000..242f32c67f8 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/ruby/PlatformRackBridgeTest.java @@ -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 index 00000000000..527f48fc104 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/ruby/PlatformRubyBridgeTest.java @@ -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 index 00000000000..e1cc2ca163d --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/ruby/database_version.rb @@ -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