Repository: brooklyn-server Updated Branches: refs/heads/master 3ac961dbc -> 05ae90a71
http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/9b337035/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/BundleAndTypeAndSubtypeResourcesTest.java ---------------------------------------------------------------------- diff --git a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/BundleAndTypeAndSubtypeResourcesTest.java b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/BundleAndTypeAndSubtypeResourcesTest.java new file mode 100644 index 0000000..03578f1 --- /dev/null +++ b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/BundleAndTypeAndSubtypeResourcesTest.java @@ -0,0 +1,1093 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.brooklyn.rest.resources; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.awt.Image; +import java.awt.Toolkit; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.objs.BrooklynObject; +import org.apache.brooklyn.api.objs.Configurable; +import org.apache.brooklyn.api.objs.Identifiable; +import org.apache.brooklyn.api.policy.Policy; +import org.apache.brooklyn.api.typereg.ManagedBundle; +import org.apache.brooklyn.api.typereg.OsgiBundleWithUrl; +import org.apache.brooklyn.api.typereg.RegisteredType; +import org.apache.brooklyn.core.entity.EntityPredicates; +import org.apache.brooklyn.core.mgmt.ha.OsgiManager; +import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; +import org.apache.brooklyn.core.mgmt.osgi.OsgiStandaloneTest; +import org.apache.brooklyn.core.test.entity.TestEntity; +import org.apache.brooklyn.enricher.stock.Aggregator; +import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy; +import org.apache.brooklyn.rest.domain.BundleInstallationRestResult; +import org.apache.brooklyn.rest.domain.BundleSummary; +import org.apache.brooklyn.rest.domain.TypeDetail; +import org.apache.brooklyn.rest.domain.TypeSummary; +import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.test.support.TestResourceUnavailableException; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.core.ResourceUtils; +import org.apache.brooklyn.util.core.osgi.BundleMaker; +import org.apache.brooklyn.util.javalang.JavaClassNames; +import org.apache.brooklyn.util.javalang.Reflections; +import org.apache.brooklyn.util.os.Os; +import org.apache.brooklyn.util.osgi.OsgiTestResources; +import org.apache.brooklyn.util.stream.Streams; +import org.apache.http.HttpHeaders; +import org.eclipse.jetty.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; +import org.testng.reporters.Files; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +public class BundleAndTypeAndSubtypeResourcesTest extends BrooklynRestResourceTest { + + private static final Logger log = LoggerFactory.getLogger(BundleAndTypeAndSubtypeResourcesTest.class); + + private static String TEST_VERSION = "0.1.2"; + private static String TEST_LASTEST_VERSION = "0.1.3"; + + private Collection<ManagedBundle> initialBundles; + + @Override + protected boolean useLocalScannedCatalog() { + return true; + } + + @Override + protected void initClass() throws Exception { + super.initClass(); + // cache initially installed bundles + OsgiManager osgi = ((ManagementContextInternal)getManagementContext()).getOsgiManager().get(); + initialBundles = osgi.getManagedBundles().values(); + } + + protected void initMethod() throws Exception { + super.initMethod(); + + // and reset OSGi container + OsgiManager osgi = ((ManagementContextInternal)getManagementContext()).getOsgiManager().get(); + for (ManagedBundle b: osgi.getManagedBundles().values()) { + if (!initialBundles.contains(b)) { + osgi.uninstallUploadedBundle(b); + } + } + } + + @Test + /** based on CampYamlLiteTest */ + public void testRegisterCustomEntityTopLevelSyntaxWithBundleWhereEntityIsFromCoreAndIconFromBundle() { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH); + + String symbolicName = "my.catalog.entity.id"; + String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: entity", + " name: My Catalog App", + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " libraries:", + " - url: " + bundleUrl, + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"); + + Response response = client().path("/bundles") + .post(yaml); + + assertEquals(response.getStatus(), Response.Status.CREATED.getStatusCode()); + BundleInstallationRestResult installed = response.readEntity(BundleInstallationRestResult.class); + Asserts.assertSize(installed.getTypes().values(), 1); + TypeSummary installedItem = installed.getTypes().get(symbolicName+":"+TEST_VERSION); + Assert.assertNotNull(installedItem, ""+installed.getTypes()); + + TypeDetail entityItem = client().path("/types/"+symbolicName + "/" + TEST_VERSION) + .get(TypeDetail.class); + + Assert.assertEquals(new TypeSummary(entityItem), installedItem); + Assert.assertNotNull(entityItem.getPlan()); + Assert.assertTrue(((String)entityItem.getPlan().getData()).contains("org.apache.brooklyn.core.test.entity.TestEntity")); + + assertEquals(entityItem.getSymbolicName(), symbolicName); + assertEquals(entityItem.getVersion(), TEST_VERSION); + + // also check it's included in various lists + List<TypeSummary> list1 = client().path("/types/"+symbolicName).get(new GenericType<List<TypeSummary>>() {}); + assertEquals(list1, MutableList.of(installedItem)); + List<TypeSummary> list2 = client().path("/types").get(new GenericType<List<TypeSummary>>() {}); + Assert.assertTrue(list2.contains(installedItem), ""+list2); + List<TypeSummary> list3 = client().path("/subtypes/entity").get(new GenericType<List<TypeSummary>>() {}); + Assert.assertTrue(list3.contains(installedItem), ""+list3); + + // and internally let's check we have libraries + RegisteredType item = getManagementContext().getTypeRegistry().get(symbolicName, TEST_VERSION); + Assert.assertNotNull(item); + Collection<OsgiBundleWithUrl> libs = item.getLibraries(); + assertEquals(libs.size(), 1); + assertEquals(Iterables.getOnlyElement(libs).getUrl(), bundleUrl); + + // now let's check other things on the item + URI expectedIconUrl = URI.create(getEndpointAddress() + "/types/" + symbolicName + "/" + entityItem.getVersion()+"/icon").normalize(); + assertEquals(entityItem.getDisplayName(), "My Catalog App"); + assertEquals(entityItem.getDescription(), "My description"); + assertEquals(entityItem.getIconUrl(), expectedIconUrl.getPath()); + assertEquals(item.getIconUrl(), "classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif"); + + // an InterfacesTag should be created for every catalog item + if (checkTraits(false)) { + @SuppressWarnings("unchecked") + Map<String, List<String>> traitsMapTag = Iterables.getOnlyElement(Iterables.filter(entityItem.getTags(), Map.class)); + List<String> actualInterfaces = traitsMapTag.get("traits"); + List<Class<?>> expectedInterfaces = Reflections.getAllInterfaces(TestEntity.class); + assertEquals(actualInterfaces.size(), expectedInterfaces.size()); + for (Class<?> expectedInterface : expectedInterfaces) { + assertTrue(actualInterfaces.contains(expectedInterface.getName())); + } + } + + byte[] iconData = client().path("/types/" + symbolicName + "/" + TEST_VERSION+"/icon").get(byte[].class); + assertEquals(iconData.length, 43); + } + + @Test + public void testRegisterOsgiPolicyTopLevelSyntax() { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH); + + String symbolicName = "my.catalog.entity.id."+JavaClassNames.niceClassAndMethod(); + String policyType = "org.apache.brooklyn.test.osgi.entities.SimplePolicy"; + String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; + + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: policy", + " name: My Catalog App", + " description: My description", + " libraries:", + " - url: " + bundleUrl, + " item:", + " type: " + policyType); + + TypeSummary installedItem = Iterables.getOnlyElement( client().path("/bundles") + .post(yaml, BundleInstallationRestResult.class).getTypes().values() ); + + assertEquals(installedItem.getSymbolicName(), symbolicName); + assertEquals(installedItem.getVersion(), TEST_VERSION); + Assert.assertTrue(installedItem.getSupertypes().contains(Policy.class.getName()), ""+installedItem.getSupertypes()); + } + + @Test + public void testFilterListOfEntitiesByName() { + List<TypeSummary> entities = client().path("/types") + .query("fragment", "vaNIllasOFTWAREpROCESS").get(new GenericType<List<TypeSummary>>() {}); + log.info("Matching entities: " + entities); + assertEquals(entities.size(), 1); + + entities = client().path("/subtypes/entity") + .query("fragment", "vaNIllasOFTWAREpROCESS").get(new GenericType<List<TypeSummary>>() {}); + log.info("Matching entities: " + entities); + assertEquals(entities.size(), 1); + + List<TypeSummary> entities2 = client().path("/types") + .query("regex", "[Vv]an.[alS]+oftware\\w+").get(new GenericType<List<TypeSummary>>() {}); + assertEquals(entities2.size(), 1); + + assertEquals(entities, entities2); + + entities = client().path("/subtypes/entity") + .query("fragment", "bweqQzZ").get(new GenericType<List<TypeSummary>>() {}); + Asserts.assertSize(entities, 0); + + entities = client().path("/subtypes/entity") + .query("regex", "bweq+z+").get(new GenericType<List<TypeSummary>>() {}); + Asserts.assertSize(entities, 0); + } + + @Test + public void testGetCatalogEntityIconDetails() throws IOException { + String catalogItemId = "testGetCatalogEntityIconDetails"; + addTestCatalogItemAsEntity(catalogItemId); + Response response = client().path(URI.create("/types/" + catalogItemId + "/" + TEST_VERSION + "/icon")) + .get(); + response.bufferEntity(); + Assert.assertEquals(response.getStatus(), 200); + Assert.assertEquals(response.getMediaType(), MediaType.valueOf("image/png")); + Image image = Toolkit.getDefaultToolkit().createImage(Files.readFile(response.readEntity(InputStream.class))); + Assert.assertNotNull(image); + } + + private void addTestCatalogItemAsEntity(String catalogItemId) { + addTestCatalogItem(catalogItemId, "entity", TEST_VERSION, "org.apache.brooklyn.rest.resources.DummyIconEntity"); + } + + private void addTestCatalogItem(String catalogItemId, String itemType, String version, String service) { + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + catalogItemId, + " version: " + TEST_VERSION, + " itemType: " + checkNotNull(itemType), + " name: My Catalog App", + " description: My description", + " icon_url: classpath:///bridge-small.png", + " version: " + version, + " item:", + " type: " + service); + + client().path("/bundles").post(yaml); + } + + @Test + public void testListPolicies() { + Set<TypeSummary> policies = client().path("/subtypes/policy") + .get(new GenericType<Set<TypeSummary>>() {}); + + assertTrue(policies.size() > 0); + TypeSummary asp = null; + for (TypeSummary p : policies) { + if (AutoScalerPolicy.class.getName().equals(p.getSymbolicName())) + asp = p; + } + Assert.assertNotNull(asp, "didn't find AutoScalerPolicy"); + } + + @Test + public void testLocationAddGetAndRemove() { + String symbolicName = "my.catalog.location.id"; + String locationType = "localhost"; + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: location", + " name: My Catalog Location", + " description: My description", + " item:", + " type: " + locationType); + + // Create location item + Map<String, TypeSummary> items = client().path("/bundles") + .post(yaml, BundleInstallationRestResult.class).getTypes(); + TypeSummary locationItem = Iterables.getOnlyElement(items.values()); + + assertEquals(locationItem.getSymbolicName(), symbolicName); + assertEquals(locationItem.getVersion(), TEST_VERSION); + + // Retrieve location item + TypeDetail location = client().path("/types/"+symbolicName+"/"+TEST_VERSION).get(TypeDetail.class); + assertEquals(location.getSymbolicName(), symbolicName); + + // Retrieve all locations + Set<TypeSummary> locations = client().path("/subtypes/location") + .get(new GenericType<Set<TypeSummary>>() {}); + boolean found = false; + for (TypeSummary contender : locations) { + if (contender.getSymbolicName().equals(symbolicName)) { + found = true; + break; + } + } + Assert.assertTrue(found, "contenders="+locations); + + // Delete + Response deleteResponse = client().path("/bundles/"+locationItem.getContainingBundle().replaceAll(":", "/")) + .delete(); + assertEquals(deleteResponse.getStatus(), Response.Status.OK.getStatusCode()); + BundleInstallationRestResult deletionResponse = deleteResponse.readEntity(BundleInstallationRestResult.class); + Assert.assertEquals(deletionResponse.getBundle(), symbolicName+":"+TEST_VERSION); + Assert.assertEquals(deletionResponse.getTypes().keySet(), MutableSet.of(symbolicName+":"+TEST_VERSION)); + + Response getPostDeleteResponse = client().path("/types/"+symbolicName+"/"+TEST_VERSION) + .get(); + assertEquals(getPostDeleteResponse.getStatus(), Response.Status.NOT_FOUND.getStatusCode()); + } + + @Test + public void testListEnrichers() { + Set<TypeSummary> enrichers = client().path("/subtypes/enricher") + .get(new GenericType<Set<TypeSummary>>() {}); + + assertTrue(enrichers.size() > 0); + TypeSummary asp = null; + for (TypeSummary p : enrichers) { + if (Aggregator.class.getName().equals(p.getSymbolicName())) + asp = p; + } + Assert.assertNotNull(asp, "didn't find Aggregator"); + } + + @Test + public void testEnricherAddGet() { + String symbolicName = "my.catalog.enricher.id"; + String enricherType = "org.apache.brooklyn.enricher.stock.Aggregator"; + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: enricher", + " name: My Catalog Enricher", + " description: My description", + " item:", + " type: " + enricherType); + + // Create location item + Map<String, TypeSummary> items = client().path("/bundles") + .post(yaml, BundleInstallationRestResult.class).getTypes(); + TypeSummary enricherItem = Iterables.getOnlyElement(items.values()); + + assertEquals(enricherItem.getSymbolicName(), symbolicName); + assertEquals(enricherItem.getVersion(), TEST_VERSION); + + // Retrieve location item + TypeSummary enricher = client().path("/types/"+symbolicName+"/"+TEST_VERSION) + .get(TypeSummary.class); + assertEquals(enricher.getSymbolicName(), symbolicName); + + // Retrieve all locations + Set<TypeSummary> enrichers = client().path("/subtypes/enricher") + .get(new GenericType<Set<TypeSummary>>() {}); + boolean found = false; + for (TypeSummary contender : enrichers) { + if (contender.getSymbolicName().equals(symbolicName)) { + found = true; + break; + } + } + Assert.assertTrue(found, "contenders="+enrichers); + } + + @Test + // osgi may fail in IDE, typically works on mvn CLI though + public void testRegisterOsgiEnricherTopLevelSyntax() { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH); + + String symbolicName = "my.catalog.enricher.id"; + String enricherType = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENRICHER; + String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; + + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: enricher", + " name: My Catalog Enricher", + " description: My description", + " libraries:", + " - url: " + bundleUrl, + " item:", + " type: " + enricherType); + + TypeSummary installedItem = Iterables.getOnlyElement( client().path("/bundles") + .post(yaml, BundleInstallationRestResult.class).getTypes().values() ); + + assertEquals(installedItem.getSymbolicName(), symbolicName); + assertEquals(installedItem.getVersion(), TEST_VERSION); + } + + @Test + public void testDeleteCustomEntityFromCatalog() { + String symbolicName = "my.catalog.app.id.to.subsequently.delete"; + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: entity", + " name: My Catalog App To Be Deleted", + " description: My description", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"); + + client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/yaml") + .post(yaml); + + BundleSummary getInstalledBundle = client().path("/bundles/"+symbolicName+"/"+TEST_VERSION) + .get(BundleSummary.class); + assertEquals(getInstalledBundle.getSymbolicName(), symbolicName); + assertEquals(getInstalledBundle.getVersion(), TEST_VERSION); + Asserts.assertNotNull(getInstalledBundle.getTypes(), "expected 'types' in: "+getInstalledBundle.getExtraFields()); + Asserts.assertStringContains(""+getInstalledBundle.getTypes(), "My Catalog App"); + + Response deleteResponse = client().path("/bundles/"+symbolicName+"/"+TEST_VERSION) + .delete(); + + assertEquals(deleteResponse.getStatus(), Response.Status.OK.getStatusCode()); + // contents of delete tested in delete location method + + Response getPostDeleteResponse = client().path("/bundles/"+symbolicName+"/"+TEST_VERSION) + .get(); + assertEquals(getPostDeleteResponse.getStatus(), Response.Status.NOT_FOUND.getStatusCode()); + } + + private void addCatalogItemWithInvalidBundleUrl(String bundleUrl) { + String symbolicName = "my.catalog.entity.id"; + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: entity", + " name: My Catalog App", + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " libraries:", + " - url: " + bundleUrl, + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"); + + Response response = client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-yaml") + .post(yaml); + + assertEquals(response.getStatus(), HttpStatus.BAD_REQUEST_400); + } + + @Test + public void testAddUnreachableItem() { + addCatalogItemWithInvalidBundleUrl("http://0.0.0.0/can-not-connect"); + } + + @Test + public void testAddInvalidItem() { + //equivalent to HTTP response 200 text/html + addCatalogItemWithInvalidBundleUrl("classpath://not-a-jar-file.txt"); + } + + @Test + public void testAddMissingItem() { + //equivalent to HTTP response 404 text/html + addCatalogItemWithInvalidBundleUrl("classpath://missing-jar-file.txt"); + } + + @Test + public void testInvalidArchive() throws Exception { + File f = Os.newTempFile("osgi", "zip"); + + Response response = client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), "zip file is empty"); + } + + @Test + public void testArchiveWithoutBom() throws Exception { + File f = createZip(ImmutableMap.<String, String>of()); + + Response response = client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), "Missing bundle symbolic name in BOM or MANIFEST"); + } + + @Test + public void testArchiveWithoutBundleAndVersion() throws Exception { + File f = createZip(ImmutableMap.<String, String>of("catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " itemType: entity", + " name: My Catalog App", + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"))); + + Response response = client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), "Missing bundle symbolic name in BOM or MANIFEST"); + } + + @Test + public void testArchiveWithoutBundle() throws Exception { + File f = createZip(ImmutableMap.<String, String>of("catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " version: 0.1.0", + " itemType: entity", + " name: My Catalog App", + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"))); + + Response response = client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), + "Missing bundle symbolic name in BOM or MANIFEST"); + } + + @Test + public void testArchiveWithoutVersion() throws Exception { + File f = createZip(ImmutableMap.<String, String>of("catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: org.apache.brooklyn.test", + " itemType: entity", + " name: My Catalog App", + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"))); + + Response response = client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), "Catalog BOM must define version"); + } + + @Test + public void testJarWithoutMatchingBundle() throws Exception { + String name = "My Catalog App"; + String bundle = "org.apache.brooklyn.test"; + String version = "0.1.0"; + String wrongBundleName = "org.apache.brooklyn.test2"; + File f = createJar(ImmutableMap.<String, String>of( + "catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + bundle, + " version: " + version, + " itemType: entity", + " name: " + name, + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"), + "META-INF/MANIFEST.MF", Joiner.on("\n").join( + "Manifest-Version: 1.0", + "Bundle-Name: " + name, + "Bundle-SymbolicName: "+wrongBundleName, + "Bundle-Version: " + version, + "Bundle-ManifestVersion: " + version))); + + Response response = client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-jar") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), + "symbolic name mismatch", + wrongBundleName, bundle); + } + + @Test + public void testJarWithoutMatchingVersion() throws Exception { + String name = "My Catalog App"; + String bundle = "org.apache.brooklyn.test"; + String version = "0.1.0"; + String wrongVersion = "0.3.0"; + File f = createJar(ImmutableMap.<String, String>of( + "catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + bundle, + " version: " + version, + " itemType: entity", + " name: " + name, + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"), + "META-INF/MANIFEST.MF", Joiner.on("\n").join( + "Manifest-Version: 1.0", + "Bundle-Name: " + name, + "Bundle-SymbolicName: " + bundle, + "Bundle-Version: " + wrongVersion, + "Bundle-ManifestVersion: " + wrongVersion))); + + Response response = client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-jar") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), + "version mismatch", + wrongVersion, version); + } + + @Test + public void testOsgiBundleWithBom() throws Exception { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH); + final String symbolicName = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_SYMBOLIC_NAME_FULL; + final String version = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_VERSION; + final String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; + BundleMaker bm = new BundleMaker(manager); + File f = Os.newTempFile("osgi", "jar"); + Files.copyFile(ResourceUtils.create(this).getResourceFromUrl(bundleUrl), f); + + String bom = Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + symbolicName, + " version: " + version, + " id: " + symbolicName, + " itemType: entity", + " name: My Catalog App", + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"); + + f = bm.copyAdding(f, MutableMap.of(new ZipEntry("catalog.bom"), (InputStream) new ByteArrayInputStream(bom.getBytes()))); + + Response response = client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-jar") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.CREATED.getStatusCode()); + + TypeSummary entityItem = client().path("/types/"+symbolicName + "/" + version) + .get(TypeSummary.class); + + assertEquals(entityItem.getSymbolicName(), symbolicName); + assertEquals(entityItem.getVersion(), version); + + // and internally let's check we have libraries + RegisteredType item = getManagementContext().getTypeRegistry().get(symbolicName, version); + Assert.assertNotNull(item); + Collection<OsgiBundleWithUrl> libs = item.getLibraries(); + assertEquals(libs.size(), 1); + OsgiBundleWithUrl lib = Iterables.getOnlyElement(libs); + Assert.assertNull(lib.getUrl()); + + assertEquals(lib.getSymbolicName(), "org.apache.brooklyn.test.resources.osgi.brooklyn-test-osgi-entities"); + assertEquals(lib.getSuppliedVersionString(), version); + + // now let's check other things on the item + URI expectedIconUrl = URI.create(getEndpointAddress() + "/types/" + symbolicName + "/" + entityItem.getVersion()+"/icon").normalize(); + assertEquals(entityItem.getDisplayName(), "My Catalog App"); + assertEquals(entityItem.getDescription(), "My description"); + assertEquals(entityItem.getIconUrl(), expectedIconUrl.getPath()); + assertEquals(item.getIconUrl(), "classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif"); + + if (checkTraits(false)) { + // an InterfacesTag should be created for every catalog item + @SuppressWarnings("unchecked") + Map<String, List<String>> traitsMapTag = Iterables.getOnlyElement(Iterables.filter(entityItem.getTags(), Map.class)); + List<String> actualInterfaces = traitsMapTag.get("traits"); + List<Class<?>> expectedInterfaces = Reflections.getAllInterfaces(TestEntity.class); + assertEquals(actualInterfaces.size(), expectedInterfaces.size()); + for (Class<?> expectedInterface : expectedInterfaces) { + assertTrue(actualInterfaces.contains(expectedInterface.getName())); + } + } + + byte[] iconData = client().path("/types/" + symbolicName + "/" + version + "/icon").get(byte[].class); + assertEquals(iconData.length, 43); + } + + @Test + public void testOsgiBundleWithBomNotInBrooklynNamespace() throws Exception { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_PATH); + final String symbolicName = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_SYMBOLIC_NAME_FULL; + final String version = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_VERSION; + final String bundleUrl = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_URL; + final String entityType = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_ENTITY; + final String iconPath = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_ICON_PATH; + BundleMaker bm = new BundleMaker(manager); + File f = Os.newTempFile("osgi", "jar"); + Files.copyFile(ResourceUtils.create(this).getResourceFromUrl(bundleUrl), f); + + String bom = Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + symbolicName, + " version: " + version, + " id: " + symbolicName, + " itemType: entity", + " name: My Catalog App", + " description: My description", + " icon_url: classpath:" + iconPath, + " item:", + " type: " + entityType); + + f = bm.copyAdding(f, MutableMap.of(new ZipEntry("catalog.bom"), (InputStream) new ByteArrayInputStream(bom.getBytes()))); + + Response response = client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(f))); + + + assertEquals(response.getStatus(), Response.Status.CREATED.getStatusCode()); + + TypeDetail entityItem = client().path("/types/"+symbolicName + "/" + version) + .get(TypeDetail.class); + + Assert.assertNotNull(entityItem.getPlan().getData()); + Assert.assertTrue(entityItem.getPlan().getData().toString().contains(entityType)); + + assertEquals(entityItem.getSymbolicName(), symbolicName); + assertEquals(entityItem.getVersion(), version); + + // and internally let's check we have libraries + RegisteredType item = getManagementContext().getTypeRegistry().get(symbolicName, version); + Assert.assertNotNull(item); + Collection<OsgiBundleWithUrl> libs = item.getLibraries(); + assertEquals(libs.size(), 1); + OsgiBundleWithUrl lib = Iterables.getOnlyElement(libs); + Assert.assertNull(lib.getUrl()); + + assertEquals(lib.getSymbolicName(), symbolicName); + assertEquals(lib.getSuppliedVersionString(), version); + + // now let's check other things on the item + assertEquals(entityItem.getDescription(), "My description"); + URI expectedIconUrl = URI.create(getEndpointAddress() + "/types/" + symbolicName + "/" + entityItem.getVersion() + "/icon").normalize(); + assertEquals(entityItem.getIconUrl(), expectedIconUrl.getPath()); + assertEquals(item.getIconUrl(), "classpath:" + iconPath); + + if (checkTraits(false)) { + // an InterfacesTag should be created for every catalog item + @SuppressWarnings("unchecked") + Map<String, List<String>> traitsMapTag = Iterables.getOnlyElement(Iterables.filter(entityItem.getTags(), Map.class)); + List<String> actualInterfaces = traitsMapTag.get("traits"); + List<String> expectedInterfaces = ImmutableList.of(Entity.class.getName(), BrooklynObject.class.getName(), Identifiable.class.getName(), Configurable.class.getName()); + assertTrue(actualInterfaces.containsAll(expectedInterfaces), "actual="+actualInterfaces); + } + + byte[] iconData = client().path("/types/" + symbolicName + "/" + version + "/icon").get(byte[].class); + assertEquals(iconData.length, 43); + + // Check that the catalog item is useable (i.e. can deploy the entity) + String appYaml = Joiner.on("\n").join( + "services:", + "- type: " + symbolicName + ":" + version, + " name: myEntityName"); + + Response appResponse = client().path("/applications") + .header(HttpHeaders.CONTENT_TYPE, "application/x-yaml") + .post(appYaml); + + assertEquals(appResponse.getStatus(), Response.Status.CREATED.getStatusCode()); + + Entity entity = Iterables.tryFind(getManagementContext().getEntityManager().getEntities(), EntityPredicates.displayNameEqualTo("myEntityName")).get(); + assertEquals(entity.getEntityType().getName(), entityType); + } + + private static File createZip(Map<String, String> files) throws Exception { + File f = Os.newTempFile("osgi", "zip"); + + ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(f)); + + for (Map.Entry<String, String> entry : files.entrySet()) { + ZipEntry ze = new ZipEntry(entry.getKey()); + zip.putNextEntry(ze); + zip.write(entry.getValue().getBytes()); + } + + zip.closeEntry(); + zip.flush(); + zip.close(); + + return f; + } + + private static File createJar(Map<String, String> files) throws Exception { + File f = Os.newTempFile("osgi", "jar"); + + JarOutputStream zip = new JarOutputStream(new FileOutputStream(f)); + + for (Map.Entry<String, String> entry : files.entrySet()) { + JarEntry ze = new JarEntry(entry.getKey()); + zip.putNextEntry(ze); + zip.write(entry.getValue().getBytes()); + } + + zip.closeEntry(); + zip.flush(); + zip.close(); + + return f; + } + + @Test + public void testGetOnlyLatestApplication() { + String symbolicName = "latest.catalog.application.id"; + String itemType = "template"; + String serviceType = "org.apache.brooklyn.core.test.entity.TestEntity"; + + addTestCatalogItem(symbolicName, itemType, TEST_VERSION, serviceType); + addTestCatalogItem(symbolicName, itemType, TEST_LASTEST_VERSION, serviceType); + + TypeSummary application = client().path("/types/" + symbolicName + "/latest") + .get(TypeSummary.class); + assertEquals(application.getVersion(), TEST_LASTEST_VERSION); + } + + @Test + public void testGetOnlyLatestDifferentCases() { + // depends on installation of this + testGetOnlyLatestApplication(); + + String symbolicName = "latest.catalog.application.id"; + + TypeSummary application = client().path("/types/" + symbolicName + "/LaTeSt") + .get(TypeSummary.class); + assertEquals(application.getVersion(), TEST_LASTEST_VERSION); + + application = client().path("/types/" + symbolicName + "/LATEST") + .get(TypeSummary.class); + assertEquals(application.getVersion(), TEST_LASTEST_VERSION); + } + + @Test + public void testGetOnlyLatestEntity() { + String symbolicName = "latest.catalog.entity.id"; + String itemType = "entity"; + String serviceType = "org.apache.brooklyn.core.test.entity.TestEntity"; + + addTestCatalogItem(symbolicName, itemType, TEST_VERSION, serviceType); + addTestCatalogItem(symbolicName, itemType, TEST_LASTEST_VERSION, serviceType); + + TypeSummary application = client().path("/types/" + symbolicName + "/latest") + .get(TypeSummary.class); + assertEquals(application.getVersion(), TEST_LASTEST_VERSION); + } + + @Test + public void testGetOnlyLatestLocation() { + String symbolicName = "latest.catalog.location.id"; + String itemType = "location"; + String serviceType = "localhost"; + + addTestCatalogItem(symbolicName, itemType, TEST_VERSION, serviceType); + addTestCatalogItem(symbolicName, itemType, TEST_LASTEST_VERSION, serviceType); + + TypeSummary application = client().path("/types/" + symbolicName + "/latest") + .get(TypeSummary.class); + assertEquals(application.getVersion(), TEST_LASTEST_VERSION); + } + + + @Test + public void testForceUpdateForYAML() { + String symbolicName = "force.update.catalog.application.id"; + String itemType = "template"; + String initialName = "My Catalog App"; + String initialDescription = "My description"; + String updatedName = initialName + " 2"; + String updatedDescription = initialDescription + " 2"; + + String initialYaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: " + itemType, + " name: " + initialName, + " description: " + initialDescription, + " icon_url: classpath:///bridge-small.png", + " version: " + TEST_VERSION, + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"); + String updatedYaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: " + itemType, + " name: " + updatedName, + " description: " + updatedDescription, + " icon_url: classpath:///bridge-small.png", + " version: " + TEST_VERSION, + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"); + + client().path("/bundles").post(initialYaml); + + TypeDetail initialApplication = client().path("/types/" + symbolicName + "/" + TEST_VERSION) + .get(TypeDetail.class); + assertEquals(initialApplication.getDisplayName(), initialName); + assertEquals(initialApplication.getDescription(), initialDescription); + + Response invalidResponse = client().path("/bundles").post(updatedYaml); + + assertEquals(invalidResponse.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + + Response validResponse = client().path("/bundles").query("force", true).post(updatedYaml); + + assertEquals(validResponse.getStatus(), Response.Status.CREATED.getStatusCode()); + + TypeSummary application = client().path("/types/" + symbolicName + "/" + TEST_VERSION) + .get(TypeSummary.class); + assertEquals(application.getDisplayName(), updatedName); + assertEquals(application.getDescription(), updatedDescription); + } + + @Test + public void testForceUpdateForZip() throws Exception { + final String symbolicName = "force.update.zip.catalog.application.id"; + final String initialName = "My Catalog App"; + final String initialDescription = "My Description"; + final String updatedName = initialName + " 2"; + final String updatedDescription = initialDescription +" 2"; + + File initialZip = createZip(ImmutableMap.<String, String>of("catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + symbolicName, + " version: " + TEST_VERSION, + " id: " + symbolicName, + " itemType: entity", + " name: " + initialName, + " description: " + initialDescription, + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"))); + File updatedZip = createZip(ImmutableMap.<String, String>of("catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + symbolicName, + " version: " + TEST_VERSION, + " id: " + symbolicName, + " itemType: entity", + " name: " + updatedName, + " description: " + updatedDescription, + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"))); + + client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(initialZip))); + + TypeSummary initialEntity = client().path("/types/" + symbolicName + "/" + TEST_VERSION) + .get(TypeSummary.class); + assertEquals(initialEntity.getDisplayName(), initialName); + assertEquals(initialEntity.getDescription(), initialDescription); + + Response invalidResponse = client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(updatedZip))); + + assertEquals(invalidResponse.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + + Response validResponse = client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .query("force", true) + .post(Streams.readFully(new FileInputStream(updatedZip))); + + assertEquals(validResponse.getStatus(), Response.Status.CREATED.getStatusCode()); + + TypeSummary entity = client().path("/types/" + symbolicName + "/" + TEST_VERSION) + .get(TypeSummary.class); + assertEquals(entity.getDisplayName(), updatedName); + assertEquals(entity.getDescription(), updatedDescription); + } + + @Test + public void testForceUpdateForJar() throws Exception { + final String symbolicName = "force.update.jar.catalog.application.id"; + final String initialName = "My Catalog App"; + final String initialDescription = "My Description"; + final String updatedName = initialName + " 2"; + final String updatedDescription = initialDescription +" 2"; + + File initialJar = createJar(ImmutableMap.<String, String>of("catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + symbolicName, + " version: " + TEST_VERSION, + " id: " + symbolicName, + " itemType: entity", + " name: " + initialName, + " description: " + initialDescription, + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"))); + File updatedJar = createJar(ImmutableMap.<String, String>of("catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + symbolicName, + " version: " + TEST_VERSION, + " id: " + symbolicName, + " itemType: entity", + " name: " + updatedName, + " description: " + updatedDescription, + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"))); + + client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-jar") + .post(Streams.readFully(new FileInputStream(initialJar))); + + TypeSummary initialEntity = client().path("/types/" + symbolicName + "/" + TEST_VERSION) + .get(TypeSummary.class); + assertEquals(initialEntity.getDisplayName(), initialName); + assertEquals(initialEntity.getDescription(), initialDescription); + + Response invalidResponse = client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-jar") + .post(Streams.readFully(new FileInputStream(updatedJar))); + + assertEquals(invalidResponse.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + + Response validResponse = client().path("/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-jar") + .query("force", true) + .post(Streams.readFully(new FileInputStream(updatedJar))); + + assertEquals(validResponse.getStatus(), Response.Status.CREATED.getStatusCode()); + + TypeSummary entity = client().path("/types/" + symbolicName + "/" + TEST_VERSION) + .get(TypeSummary.class); + assertEquals(entity.getDisplayName(), updatedName); + assertEquals(entity.getDescription(), updatedDescription); + } + + // TODO traits no longer always set - we have supertypes so not needed, we should investigate when they are and when they aren't + // and switch those to setting and using the supertypes + private boolean checkTraits(boolean currentExpectedToBeWorking) { + return currentExpectedToBeWorking; + } + +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/9b337035/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java b/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java index 9fec03c..9734153 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java @@ -17,19 +17,24 @@ package org.apache.brooklyn.util.osgi; import static com.google.common.base.Preconditions.checkNotNull; +import java.util.Comparator; + import javax.annotation.Nullable; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.text.BrooklynVersionSyntax; +import org.apache.brooklyn.util.text.NaturalOrderComparator; import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.text.VersionComparator; import org.osgi.framework.Bundle; import org.osgi.framework.Version; import com.google.common.base.Objects; +import com.google.common.collect.ComparisonChain; /** Records a name (string) and version (string), * with conveniences for pretty-printing and converting to OSGi format. */ -public class VersionedName { +public class VersionedName implements Comparable<VersionedName> { private final String name; private final String v; @@ -172,4 +177,21 @@ public class VersionedName { return Maybe.of(new VersionedName(parts[0], parts.length == 2 ? parts[1] : null)); } + @Override + public int compareTo(VersionedName other) { + return VersionedNameComparator.INSTANCE.compare(this, other); + } + + public static class VersionedNameComparator implements Comparator<VersionedName> { + public static final VersionedNameComparator INSTANCE = new VersionedNameComparator(); + + @Override + public int compare(VersionedName o1, VersionedName o2) { + return ComparisonChain.start() + .compare(o1.getSymbolicName(), o2.getSymbolicName(), NaturalOrderComparator.INSTANCE) + .compare(o2.getOsgiVersionString(), o1.getOsgiVersionString(), VersionComparator.INSTANCE) + .compare(o2.getVersionString(), o1.getVersionString(), VersionComparator.INSTANCE) + .result(); + } + } }
