diff --git a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/TypeOneUUIDGenerator.java b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/TypeOneUUIDGenerator.java new file mode 100644 index 0000000000..703c128812 --- /dev/null +++ b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/TypeOneUUIDGenerator.java @@ -0,0 +1,76 @@ +/* + * 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.nifi.util; + +import java.util.Random; +import java.util.UUID; + +public class TypeOneUUIDGenerator { + + public static final Object lock = new Object(); + + private static long lastTime; + private static long clockSequence = 0; + private static final Random randomGenerator = new Random(); + + /** + * Will generate unique time based UUID where the next UUID is always + * greater then the previous. + */ + public final static UUID generateId() { + return generateId(System.currentTimeMillis()); + } + + /** + * + */ + public final static UUID generateId(long currentTime) { + return generateId(currentTime, Math.abs(randomGenerator.nextInt())); + } + + /** + * + */ + public final static UUID generateId(long currentTime, int lsbInt) { + long time; + + synchronized (lock) { + if (currentTime > lastTime) { + lastTime = currentTime; + clockSequence = 0; + } else { + ++clockSequence; + } + } + + time = currentTime; + + // low Time + time = currentTime << 32; + + // mid Time + time |= ((currentTime & 0xFFFF00000000L) >> 16); + + // hi Time + time |= 0x1000 | ((currentTime >> 48) & 0x0FFF); + + long clockSequenceHi = clockSequence; + clockSequenceHi <<= 48; + long lsb = clockSequenceHi | lsbInt; + return new UUID(time, lsb); + } +} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowSnippetDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowSnippetDTO.java index 1ce390ed16..63194a372f 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowSnippetDTO.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowSnippetDTO.java @@ -16,11 +16,16 @@ */ package org.apache.nifi.web.api.dto; -import com.wordnik.swagger.annotations.ApiModelProperty; +import java.util.Comparator; import java.util.LinkedHashSet; import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; + import javax.xml.bind.annotation.XmlType; +import com.wordnik.swagger.annotations.ApiModelProperty; + /** * The contents of a flow snippet. */ @@ -37,6 +42,15 @@ public class FlowSnippetDTO { private Set funnels = new LinkedHashSet<>(); private Set controllerServices = new LinkedHashSet<>(); + private final boolean newTemplate; + + public FlowSnippetDTO() { + this(false); + } + + public FlowSnippetDTO(boolean newTemplate) { + this.newTemplate = newTemplate; + } /** * @return connections in this flow snippet */ @@ -48,7 +62,8 @@ public class FlowSnippetDTO { } public void setConnections(Set connections) { - this.connections = connections; + this.removeInstanceIdentifierIfNecessary(connections); + this.connections = this.orderedById(connections); } /** @@ -62,7 +77,8 @@ public class FlowSnippetDTO { } public void setInputPorts(Set inputPorts) { - this.inputPorts = inputPorts; + this.removeInstanceIdentifierIfNecessary(inputPorts); + this.inputPorts = this.orderedById(inputPorts); } /** @@ -76,7 +92,8 @@ public class FlowSnippetDTO { } public void setLabels(Set labels) { - this.labels = labels; + this.removeInstanceIdentifierIfNecessary(labels); + this.labels = this.orderedById(labels); } /** @@ -90,7 +107,8 @@ public class FlowSnippetDTO { } public void setFunnels(Set funnels) { - this.funnels = funnels; + this.removeInstanceIdentifierIfNecessary(funnels); + this.funnels = this.orderedById(funnels); } /** @@ -104,7 +122,8 @@ public class FlowSnippetDTO { } public void setOutputPorts(Set outputPorts) { - this.outputPorts = outputPorts; + this.removeInstanceIdentifierIfNecessary(outputPorts); + this.outputPorts = this.orderedById(outputPorts); } /** @@ -118,7 +137,8 @@ public class FlowSnippetDTO { } public void setProcessGroups(Set processGroups) { - this.processGroups = processGroups; + this.removeInstanceIdentifierIfNecessary(processGroups); + this.processGroups = this.orderedById(processGroups); } /** @@ -132,7 +152,8 @@ public class FlowSnippetDTO { } public void setProcessors(Set processors) { - this.processors = processors; + this.removeInstanceIdentifierIfNecessary(processors); + this.processors = this.orderedById(processors); } /** @@ -146,7 +167,8 @@ public class FlowSnippetDTO { } public void setRemoteProcessGroups(Set remoteProcessGroups) { - this.remoteProcessGroups = remoteProcessGroups; + this.removeInstanceIdentifierIfNecessary(remoteProcessGroups); + this.remoteProcessGroups = this.orderedById(remoteProcessGroups); } /** @@ -160,6 +182,40 @@ public class FlowSnippetDTO { } public void setControllerServices(Set controllerServices) { - this.controllerServices = controllerServices; + this.removeInstanceIdentifierIfNecessary(controllerServices); + this.controllerServices = this.orderedById(controllerServices); + } + + private Set orderedById(Set dtos) { + TreeSet components = new TreeSet<>(new Comparator() { + @Override + public int compare(ComponentDTO c1, ComponentDTO c2) { + return UUID.fromString(c1.getId()).compareTo(UUID.fromString(c2.getId())); + } + }); + components.addAll(dtos); + return components; + } + + private void removeInstanceIdentifierIfNecessary(Set componentDtos) { + if (this.newTemplate) { + for (ComponentDTO componentDto : componentDtos) { + UUID id = UUID.fromString(componentDto.getId()); + id = new UUID(id.getMostSignificantBits(), 0); + componentDto.setId(id.toString()); + if (componentDto instanceof ConnectionDTO) { + ConnectionDTO connectionDTO = (ConnectionDTO) componentDto; + ConnectableDTO cdto = connectionDTO.getSource(); + id = UUID.fromString(cdto.getId()); + id = new UUID(id.getMostSignificantBits(), 0); + cdto.setId(id.toString()); + + cdto = connectionDTO.getDestination(); + id = UUID.fromString(cdto.getId()); + id = new UUID(id.getMostSignificantBits(), 0); + cdto.setId(id.toString()); + } + } + } } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java index f91fac5354..8609353400 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java @@ -41,6 +41,7 @@ import org.apache.nifi.cluster.protocol.NodeIdentifier; import org.apache.nifi.events.EventReporter; import org.apache.nifi.reporting.Severity; import org.apache.nifi.util.FormatUtils; +import org.apache.nifi.util.TypeOneUUIDGenerator; import org.apache.nifi.web.security.ProxiedEntitiesUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -216,12 +217,13 @@ public class ThreadPoolRequestReplicator implements RequestReplicator { @Override public AsyncClusterResponse replicate(Set nodeIds, String method, URI uri, Object entity, Map headers, final boolean indicateReplicated) { final Map updatedHeaders = new HashMap<>(headers); - updatedHeaders.put(RequestReplicator.CLUSTER_ID_GENERATION_SEED_HEADER, UUID.randomUUID().toString()); + updatedHeaders.put(RequestReplicator.CLUSTER_ID_GENERATION_SEED_HEADER, TypeOneUUIDGenerator.generateId().toString()); if (indicateReplicated) { updatedHeaders.put(RequestReplicator.REPLICATION_INDICATOR_HEADER, "true"); } + // If the user is authenticated, add them as a proxied entity so that when the receiving NiFi receives the request, // it knows that we are acting as a proxy on behalf of the current user. final NiFiUser user = NiFiUserUtils.getNiFiUser(); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml index 3bc53603a6..8cace45bcb 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml @@ -174,6 +174,12 @@ cglib-nodep test + + org.eclipse.jgit + org.eclipse.jgit + 4.3.1.201605051710-r + test + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/persistence/TemplateSerializer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/persistence/TemplateSerializer.java index f86ab9de96..c0e62059b0 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/persistence/TemplateSerializer.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/persistence/TemplateSerializer.java @@ -26,6 +26,11 @@ import javax.xml.bind.Marshaller; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import com.sun.xml.txw2.output.IndentingXMLStreamWriter; public final class TemplateSerializer { @@ -39,12 +44,13 @@ public final class TemplateSerializer { JAXBContext context = JAXBContext.newInstance(TemplateDTO.class); Marshaller marshaller = context.createMarshaller(); - marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); - marshaller.marshal(dto, bos); + XMLOutputFactory xmlof = XMLOutputFactory.newInstance(); + XMLStreamWriter writer = new IndentingXMLStreamWriter(xmlof.createXMLStreamWriter(bos)); + marshaller.marshal(dto, writer); bos.flush(); return baos.toByteArray(); - } catch (final IOException | JAXBException e) { + } catch (final IOException | JAXBException | XMLStreamException e) { throw new FlowSerializationException(e); } finally { if (currentCl != null) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/util/SnippetUtilsSpec.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/util/SnippetUtilsSpec.groovy index 7596c84eac..588202af85 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/util/SnippetUtilsSpec.groovy +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/util/SnippetUtilsSpec.groovy @@ -43,7 +43,7 @@ class SnippetUtilsSpec extends Specification { where: nX | nY | oX | oY | fX | fY | snippet 500 | 500 | 0 | 0 | 1.5 | 1.34 | new FlowSnippetDTO() - 500 | 500 | 10 | 10 | 1.5 | 1.34 | new FlowSnippetDTO(processors: [new ProcessorDTO(position: new PositionDTO(x: 10, y: 10))]) + 500 | 500 | 10 | 10 | 1.5 | 1.34 | new FlowSnippetDTO(processors: [new ProcessorDTO(id:"c81f6810-0155-1000-0000-c4af042cb1559", position: new PositionDTO(x: 10, y: 10))]) } @Unroll @@ -69,10 +69,10 @@ class SnippetUtilsSpec extends Specification { fX | fY | snippet 1.5 | 1.34 | new FlowSnippetDTO() 1.5 | 1.34 | new FlowSnippetDTO( - processors: [new ProcessorDTO(position: new PositionDTO(x: 10, y: 10))], + processors: [new ProcessorDTO(id:"c81f6810-0155-1000-0001-c4af042cb1559", position: new PositionDTO(x: 10, y: 10))], processGroups: [ - new ProcessGroupDTO(position: new PositionDTO(x: 105, y: -10), name: 'pg2', - contents: new FlowSnippetDTO(processors: [new ProcessorDTO(name: 'proc1', position: new PositionDTO(x: 50, y: 60))]))]) + new ProcessGroupDTO(id:"c81f6a10-0155-1000-0002-c4af042cb1559", position: new PositionDTO(x: 105, y: -10), name: 'pg2', + contents: new FlowSnippetDTO(processors: [new ProcessorDTO(id:"c81f6810-0155-1000-0002-c4af042cb1559", name: 'proc1', position: new PositionDTO(x: 50, y: 60))]))]) } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/persistence/TemplateSerializerTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/persistence/TemplateSerializerTest.java new file mode 100644 index 0000000000..9ca88bcde3 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/persistence/TemplateSerializerTest.java @@ -0,0 +1,140 @@ +/* + * 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.nifi.persistence; + +import static org.junit.Assert.assertEquals; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.Unmarshaller; +import javax.xml.transform.stream.StreamSource; + +import org.apache.nifi.nar.NarClassLoader; +import org.apache.nifi.nar.NarClassLoaders; +import org.apache.nifi.util.TypeOneUUIDGenerator; +import org.apache.nifi.web.api.dto.FlowSnippetDTO; +import org.apache.nifi.web.api.dto.ProcessorDTO; +import org.apache.nifi.web.api.dto.TemplateDTO; +import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.diff.EditList; +import org.eclipse.jgit.diff.HistogramDiff; +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.diff.RawTextComparator; +import org.junit.Before; +import org.junit.Test; + +public class TemplateSerializerTest { + @Before + public void before() throws Exception { + Field initField = NarClassLoaders.class.getDeclaredField("initialized"); + setFinalField(initField, new AtomicBoolean(true)); + Field clField = NarClassLoaders.class.getDeclaredField("frameworkClassLoader"); + NarClassLoader cl = new NarClassLoader(new File(""), Thread.currentThread().getContextClassLoader()); + setFinalField(clField, new AtomicReference(cl)); + } + + @Test + public void validateDiffWithChangingComponentIdAndAdditionalElements() throws Exception { + + // Create initial template (TemplateDTO) + FlowSnippetDTO snippet = new FlowSnippetDTO(); + Set procs = new HashSet<>(); + for (int i = 4; i > 0; i--) { + ProcessorDTO procDTO = new ProcessorDTO(); + procDTO.setType("Processor" + i + ".class"); + procDTO.setId(TypeOneUUIDGenerator.generateId().toString()); + procs.add(procDTO); + } + snippet.setProcessors(procs); + TemplateDTO origTemplate = new TemplateDTO(); + origTemplate.setDescription("MyTemplate"); + origTemplate.setId("MyTemplate"); + origTemplate.setSnippet(snippet); + byte[] serTemplate = TemplateSerializer.serialize(origTemplate); + + // Deserialize Template into TemplateDTP + ByteArrayInputStream in = new ByteArrayInputStream(serTemplate); + JAXBContext context = JAXBContext.newInstance(TemplateDTO.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + JAXBElement templateElement = unmarshaller.unmarshal(new StreamSource(in), TemplateDTO.class); + TemplateDTO deserTemplate = templateElement.getValue(); + + // Modify deserialized template + FlowSnippetDTO deserSnippet = deserTemplate.getSnippet(); + Set deserProcs = deserSnippet.getProcessors(); + int c = 0; + for (ProcessorDTO processorDTO : deserProcs) { + if (c % 2 == 0) { + processorDTO.setName("Hello-" + c); + } + c++; + } + + // add new Processor + ProcessorDTO procDTO = new ProcessorDTO(); + procDTO.setType("ProcessorNew" + ".class"); + procDTO.setId(TypeOneUUIDGenerator.generateId().toString()); + deserProcs.add(procDTO); + + // Serialize modified template + byte[] serTemplate2 = TemplateSerializer.serialize(deserTemplate); + + RawText rt1 = new RawText(serTemplate); + RawText rt2 = new RawText(serTemplate2); + EditList diffList = new EditList(); + diffList.addAll(new HistogramDiff().diff(RawTextComparator.DEFAULT, rt1, rt2)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (DiffFormatter diff = new DiffFormatter(out);) { + diff.format(diffList, rt1, rt2); + + BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(out.toByteArray()), StandardCharsets.UTF_8)); + + List changes = reader.lines().peek(System.out::println) + .filter(line -> line.startsWith("+") || line.startsWith("-")).collect(Collectors.toList()); + + assertEquals("+ Hello-0", changes.get(0)); + assertEquals("+ Hello-2", changes.get(1)); + assertEquals("+ ", changes.get(2)); + assertEquals("+ ProcessorNew.class", changes.get(4)); + assertEquals("+ ", changes.get(5)); + } + } + + public static void setFinalField(Field field, Object newValue) throws Exception { + field.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.set(null, newValue); + } +} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java index 8648c2c50e..2bffb05d58 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java @@ -41,6 +41,7 @@ import org.apache.nifi.remote.exception.NotAuthorizedException; import org.apache.nifi.remote.protocol.ResponseCode; import org.apache.nifi.remote.protocol.http.HttpHeaders; import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.util.TypeOneUUIDGenerator; import org.apache.nifi.web.AuthorizableLookup; import org.apache.nifi.web.AuthorizeAccess; import org.apache.nifi.web.NiFiServiceFacade; @@ -200,7 +201,19 @@ public abstract class ApplicationResource { protected String generateUuid() { final Optional seed = getIdGenerationSeed(); - return seed.isPresent() ? UUID.nameUUIDFromBytes(seed.get().getBytes(StandardCharsets.UTF_8)).toString() : UUID.randomUUID().toString(); + UUID uuid; + if (seed.isPresent()) { + try { + UUID seedId = UUID.fromString(seed.get()); + uuid = TypeOneUUIDGenerator.generateId(seedId.getMostSignificantBits(), Math.abs(seed.get().hashCode())); + } catch (Exception e) { + logger.warn("Provided 'seed' does not represent UUID. Will not be able to extract most significant bits for ID generation."); + uuid = UUID.nameUUIDFromBytes(seed.get().getBytes(StandardCharsets.UTF_8)); + } + } else { + uuid = TypeOneUUIDGenerator.generateId(); + } + return uuid.toString(); } protected Optional getIdGenerationSeed() { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/TemplateResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/TemplateResource.java index dcff518b08..3c492fcc7a 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/TemplateResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/TemplateResource.java @@ -16,20 +16,8 @@ */ package org.apache.nifi.web.api; -import com.wordnik.swagger.annotations.Api; -import com.wordnik.swagger.annotations.ApiOperation; -import com.wordnik.swagger.annotations.ApiParam; -import com.wordnik.swagger.annotations.ApiResponse; -import com.wordnik.swagger.annotations.ApiResponses; -import com.wordnik.swagger.annotations.Authorization; -import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.authorization.Authorizer; -import org.apache.nifi.authorization.RequestAction; -import org.apache.nifi.authorization.resource.Authorizable; -import org.apache.nifi.authorization.user.NiFiUserUtils; -import org.apache.nifi.web.NiFiServiceFacade; -import org.apache.nifi.web.api.dto.TemplateDTO; -import org.apache.nifi.web.api.entity.TemplateEntity; +import java.nio.charset.StandardCharsets; +import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; @@ -42,7 +30,23 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.authorization.RequestAction; +import org.apache.nifi.authorization.resource.Authorizable; +import org.apache.nifi.authorization.user.NiFiUserUtils; +import org.apache.nifi.persistence.TemplateSerializer; +import org.apache.nifi.web.NiFiServiceFacade; +import org.apache.nifi.web.api.dto.TemplateDTO; +import org.apache.nifi.web.api.entity.TemplateEntity; + +import com.wordnik.swagger.annotations.Api; +import com.wordnik.swagger.annotations.ApiOperation; +import com.wordnik.swagger.annotations.ApiParam; +import com.wordnik.swagger.annotations.ApiResponse; +import com.wordnik.swagger.annotations.ApiResponses; +import com.wordnik.swagger.annotations.Authorization; /** * RESTful endpoint for managing a Template. @@ -168,7 +172,13 @@ public class TemplateResource extends ApplicationResource { } // generate the response - return generateOkResponse(template).header("Content-Disposition", String.format("attachment; filename=\"%s.xml\"", attachmentName)).build(); + /* + * Here instead of relying on default JAXB marshalling we are simply + * serializing template to String (formatted, indented etc) and sending + * it as part of the response. + */ + String serializedTemplate = new String(TemplateSerializer.serialize(template), StandardCharsets.UTF_8); + return generateOkResponse(serializedTemplate).header("Content-Disposition", String.format("attachment; filename=\"%s.xml\"", attachmentName)).build(); } /** diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/SnippetUtils.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/SnippetUtils.java index fc3a8516cf..62acdd9c92 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/SnippetUtils.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/SnippetUtils.java @@ -16,17 +16,21 @@ */ package org.apache.nifi.web.util; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.connectable.ConnectableType; import org.apache.nifi.connectable.Connection; @@ -74,7 +78,7 @@ public final class SnippetUtils { * @return snippet */ public FlowSnippetDTO populateFlowSnippet(final Snippet snippet, final boolean recurse, final boolean includeControllerServices) { - final FlowSnippetDTO snippetDto = new FlowSnippetDTO(); + final FlowSnippetDTO snippetDto = new FlowSnippetDTO(true); final String groupId = snippet.getParentGroupId(); final ProcessGroup processGroup = flowController.getGroup(groupId); @@ -99,9 +103,12 @@ public final class SnippetUtils { controllerServices.addAll(getControllerServices(processor.getProperties())); } } + this.normalizeCoordinates(processors); snippetDto.setProcessors(processors); } + + // add any connections if (!snippet.getConnections().isEmpty()) { final Set connections = new LinkedHashSet<>(); @@ -559,11 +566,12 @@ public final class SnippetUtils { * Generates a new id for the current id that is specified. If no seed is found, a new random id will be created. */ private String generateId(final String currentId, final String seed) { - if (seed == null) { - return UUID.randomUUID().toString(); - } else { - return UUID.nameUUIDFromBytes((currentId + seed).getBytes(StandardCharsets.UTF_8)).toString(); - } + long msb = UUID.fromString(currentId).getMostSignificantBits(); + long lsb = StringUtils.isBlank(seed) + ? Math.abs(new Random().nextInt()) + : Math.abs(ByteBuffer.wrap(seed.getBytes(StandardCharsets.UTF_8)).getInt()); + + return new UUID(msb, lsb).toString(); } /* setters */ @@ -575,4 +583,30 @@ public final class SnippetUtils { this.flowController = flowController; } + /** + * Will normalize the coordinates of the processors to ensure their + * consistency across exports. It will do so by fist calculating the + * smallest X and smallest Y and then subtracting it from all X's and Y's of + * each processor ensuring that coordinates are consistent across export + * while preserving relative locations set by the user. + */ + private void normalizeCoordinates(Collection processors) { + double smallestX = Double.MAX_VALUE; + double smallestY = Double.MAX_VALUE; + for (ProcessorDTO processor : processors) { + double d = processor.getPosition().getX(); + if (d < smallestX) { + smallestX = d; + } + d = processor.getPosition().getY(); + if (d < smallestY) { + smallestY = d; + } + } + for (ProcessorDTO processor : processors) { + processor.getPosition().setX(processor.getPosition().getX() - smallestX); + processor.getPosition().setY(processor.getPosition().getY() - smallestY); + } + } + } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/dao/impl/StandardTemplateDAOSpec.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/dao/impl/StandardTemplateDAOSpec.groovy index 31e490a2df..206be8d9f0 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/dao/impl/StandardTemplateDAOSpec.groovy +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/dao/impl/StandardTemplateDAOSpec.groovy @@ -24,6 +24,8 @@ import org.apache.nifi.controller.service.ControllerServiceProvider import org.apache.nifi.groups.ProcessGroup import org.apache.nifi.web.api.dto.* import org.apache.nifi.web.util.SnippetUtils + +import spock.lang.Ignore; import spock.lang.Specification import spock.lang.Unroll @@ -117,28 +119,28 @@ class StandardTemplateDAOSpec extends Specification { rootGroupId | oldOriginX | oldOriginY | newOriginX | newOriginY | templateId | idGenerationSeed | encodingVersion | snippet 'g1' | 0.0 | 0.0 | 5.0 | 5.0 | 't1' | 'AAAA' | null | new FlowSnippetDTO() 'g1' | 10.0 | 10.0 | 5.0 | 5.0 | 't1' | 'AAAA' | '0.7' | new FlowSnippetDTO( - processors: [new ProcessorDTO(name: 'proc1', config: new ProcessorConfigDTO(), position: new PositionDTO(x: 10, y: 10))]) + processors: [new ProcessorDTO(id:"c81f6810-0155-1000-0000-c4af042cb1559", name: 'proc1', config: new ProcessorConfigDTO(), position: new PositionDTO(x: 10, y: 10))]) 'g1' | 10.0 | -10.0 | 5.0 | 5.0 | 't1' | 'AAAA' | null | new FlowSnippetDTO( - processors: [new ProcessorDTO(name: 'proc2', config: new ProcessorConfigDTO(), position: new PositionDTO(x: 10, y: 10))], + processors: [new ProcessorDTO(id:"c81f6810-0155-1000-0001-c4af042cb1559", name: 'proc2', config: new ProcessorConfigDTO(), position: new PositionDTO(x: 10, y: 10))], processGroups: [ - new ProcessGroupDTO( + new ProcessGroupDTO(id:"c81f6810-0a55-1000-0000-c4af042cb1559", name: 'g2', position: new PositionDTO(x: 105, y: -10), - contents: new FlowSnippetDTO(processors: [new ProcessorDTO(name: 'proc3', config: new ProcessorConfigDTO(), position: new PositionDTO(x: 50, y: 60))]))]) + contents: new FlowSnippetDTO(processors: [new ProcessorDTO(id:"c81f6810-0155-1000-0002-c4af042cb1559", name: 'proc3', config: new ProcessorConfigDTO(), position: new PositionDTO(x: 50, y: 60))]))]) 'g1' | 10.0 | -10.0 | 5.0 | 5.0 | 't1' | 'AAAA' | '0.7' | new FlowSnippetDTO( - processors: [new ProcessorDTO(name: 'proc2', config: new ProcessorConfigDTO(), position: new PositionDTO(x: 10, y: 10))], + processors: [new ProcessorDTO(id:"c81f6810-0155-1000-0003-c4af042cb1559", name: 'proc2', config: new ProcessorConfigDTO(), position: new PositionDTO(x: 10, y: 10))], processGroups: [ - new ProcessGroupDTO( + new ProcessGroupDTO(id:"c81f6810-0a55-1000-0001-c4af042cb1559", name: 'g2', position: new PositionDTO(x: 105, y: -10), - contents: new FlowSnippetDTO(processors: [new ProcessorDTO(name: 'proc3', config: new ProcessorConfigDTO(), position: new PositionDTO(x: 50, y: 60))]))]) + contents: new FlowSnippetDTO(processors: [new ProcessorDTO(id:"c81f6810-0155-1000-0004-c4af042cb1559", name: 'proc3', config: new ProcessorConfigDTO(), position: new PositionDTO(x: 50, y: 60))]))]) 'g1' | 10.0 | -10.0 | 5.0 | 5.0 | 't1' | 'AAAA' | '1.0' | new FlowSnippetDTO( - processors: [new ProcessorDTO(name: 'proc2', config: new ProcessorConfigDTO(), position: new PositionDTO(x: 10, y: 10))], + processors: [new ProcessorDTO(id:"c81f6810-0155-1000-0005-c4af042cb1559", name: 'proc2', config: new ProcessorConfigDTO(), position: new PositionDTO(x: 10, y: 10))], processGroups: [ - new ProcessGroupDTO( + new ProcessGroupDTO(id:"c81f6810-0a55-1000-0003-c4af042cb1559", name: 'g2', position: new PositionDTO(x: 105, y: -10), - contents: new FlowSnippetDTO(processors: [new ProcessorDTO(name: 'proc3', config: new ProcessorConfigDTO(), position: new PositionDTO(x: 50, y: 60))]))]) + contents: new FlowSnippetDTO(processors: [new ProcessorDTO(id:"c81f6810-0155-1000-0006-c4af042cb1559", name: 'proc3', config: new ProcessorConfigDTO(), position: new PositionDTO(x: 50, y: 60))]))]) } def PositionDTO calculateMoveAndScalePosition(position, oldOriginX, oldOriginY, newOriginX, newOriginY, factorX, factorY) {