diff --git a/nifi-extension-bundles/nifi-parquet-bundle/nifi-parquet-content-viewer/pom.xml b/nifi-extension-bundles/nifi-parquet-bundle/nifi-parquet-content-viewer/pom.xml
new file mode 100644
index 000000000000..0d5fe96b02fd
--- /dev/null
+++ b/nifi-extension-bundles/nifi-parquet-bundle/nifi-parquet-content-viewer/pom.xml
@@ -0,0 +1,158 @@
+
+
+
+ 4.0.0
+
+ org.apache.nifi
+ nifi-parquet-bundle
+ 2.7.0-SNAPSHOT
+
+ nifi-parquet-content-viewer
+ war
+
+ true
+ ${project.build.directory}/standard-content-viewer-ui-working-directory
+
+
+
+ org.apache.nifi
+ nifi-framework-api
+ 2.7.0-SNAPSHOT
+ provided
+
+
+ org.apache.nifi
+ nifi-content-viewer-utils
+ 2.7.0-SNAPSHOT
+
+
+ org.apache.nifi
+ nifi-web-servlet-shared
+ 2.7.0-SNAPSHOT
+
+
+ org.apache.nifi
+ nifi-frontend
+ 2.7.0-SNAPSHOT
+
+
+ org.glassfish.jersey.core
+ jersey-common
+
+
+ org.apache.parquet
+ parquet-common
+ ${parquet.version}
+ provided
+
+
+ org.apache.parquet
+ parquet-hadoop
+ ${parquet.version}
+ provided
+
+
+ org.apache.parquet
+ parquet-avro
+ ${parquet.version}
+ provided
+
+
+ org.apache.hadoop
+ hadoop-common
+ ${hadoop.version}
+ provided
+
+
+ log4j
+ log4j
+
+
+ org.slf4j
+ slf4j-reload4j
+
+
+ org.slf4j
+ slf4j-log4j12
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+ org.apache.nifi
+ nifi-parquet-shared
+ 2.7.0-SNAPSHOT
+ provided
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ unpack-standard-content-viewer-ui
+ prepare-package
+
+ unpack-dependencies
+
+
+ org.apache.nifi
+ nifi-frontend
+ true
+ false
+ ${standard-content-viewer.ui.working.dir}
+ standard-content-viewer/**/*
+
+
+
+
+
+
+ maven-war-plugin
+
+
+
+ ${standard-content-viewer.ui.working.dir}/standard-content-viewer
+ **/*
+ WEB-INF/classes/static
+
+
+ src/main/webapp/META-INF
+ META-INF
+
+ nifi-content-viewer
+
+ false
+
+
+ WEB-INF/lib/nifi-frontend*.jar
+
+
+
+
+
diff --git a/nifi-extension-bundles/nifi-parquet-bundle/nifi-parquet-content-viewer/src/main/java/org/apache/nifi/parquet/web/content/viewer/ParquetServletContextListener.java b/nifi-extension-bundles/nifi-parquet-bundle/nifi-parquet-content-viewer/src/main/java/org/apache/nifi/parquet/web/content/viewer/ParquetServletContextListener.java
new file mode 100644
index 000000000000..18e914706635
--- /dev/null
+++ b/nifi-extension-bundles/nifi-parquet-bundle/nifi-parquet-content-viewer/src/main/java/org/apache/nifi/parquet/web/content/viewer/ParquetServletContextListener.java
@@ -0,0 +1,71 @@
+/*
+ * 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.parquet.web.content.viewer;
+
+import jakarta.servlet.DispatcherType;
+import jakarta.servlet.FilterRegistration;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+import jakarta.servlet.ServletRegistration;
+import jakarta.servlet.annotation.WebListener;
+import org.apache.nifi.web.servlet.filter.QueryStringToFragmentFilter;
+import org.apache.nifi.parquet.web.controller.ParquetContentViewerController;
+import org.eclipse.jetty.ee11.servlet.DefaultServlet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.EnumSet;
+
+/**
+ * Servlet Context Listener supporting registration of Filters
+ */
+@WebListener
+public class ParquetServletContextListener implements ServletContextListener {
+ private static final String API_CONTENT_MAPPING = "/api/content";
+
+ private static final int LOAD_ON_STARTUP_ENABLED = 1;
+
+ private static final String DIR_ALLOWED_PARAMETER = "dirAllowed";
+
+ private static final String BASE_RESOURCE_PARAMETER = "baseResource";
+
+ private static final String BASE_RESOURCE_DIRECTORY = "WEB-INF/classes/static";
+
+ private static final String DEFAULT_MAPPING = "/";
+
+ private static final Logger logger = LoggerFactory.getLogger(ParquetServletContextListener.class);
+
+ @Override
+ public void contextInitialized(final ServletContextEvent sce) {
+ final ServletContext servletContext = sce.getServletContext();
+ final FilterRegistration.Dynamic filter = servletContext.addFilter(QueryStringToFragmentFilter.class.getSimpleName(), QueryStringToFragmentFilter.class);
+ filter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, DEFAULT_MAPPING);
+
+ final ServletRegistration.Dynamic servlet = servletContext.addServlet(ParquetContentViewerController.class.getSimpleName(), ParquetContentViewerController.class);
+ servlet.addMapping(API_CONTENT_MAPPING);
+ servlet.setLoadOnStartup(LOAD_ON_STARTUP_ENABLED);
+
+ final ServletRegistration.Dynamic defaultServlet = servletContext.addServlet(DefaultServlet.class.getSimpleName(), DefaultServlet.class);
+ defaultServlet.addMapping(DEFAULT_MAPPING);
+ defaultServlet.setInitParameter(DIR_ALLOWED_PARAMETER, Boolean.FALSE.toString());
+ defaultServlet.setInitParameter(BASE_RESOURCE_PARAMETER, BASE_RESOURCE_DIRECTORY);
+ defaultServlet.setLoadOnStartup(LOAD_ON_STARTUP_ENABLED);
+
+ logger.info("Parquet Content Viewer Initialized");
+ }
+}
diff --git a/nifi-extension-bundles/nifi-parquet-bundle/nifi-parquet-content-viewer/src/main/java/org/apache/nifi/parquet/web/controller/ParquetContentViewerController.java b/nifi-extension-bundles/nifi-parquet-bundle/nifi-parquet-content-viewer/src/main/java/org/apache/nifi/parquet/web/controller/ParquetContentViewerController.java
new file mode 100644
index 000000000000..79b647d52df3
--- /dev/null
+++ b/nifi-extension-bundles/nifi-parquet-bundle/nifi-parquet-content-viewer/src/main/java/org/apache/nifi/parquet/web/controller/ParquetContentViewerController.java
@@ -0,0 +1,192 @@
+/*
+ * 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.parquet.web.controller;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.avro.Schema;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.nifi.authorization.AccessDeniedException;
+import org.apache.nifi.web.ContentAccess;
+import org.apache.nifi.web.ContentRequestContext;
+import org.apache.nifi.web.DownloadableContent;
+import org.apache.nifi.web.HttpServletContentRequestContext;
+import org.apache.nifi.web.ResourceNotFoundException;
+import org.apache.parquet.avro.AvroParquetReader;
+import org.apache.parquet.hadoop.ParquetReader;
+import org.apache.parquet.io.InputFile;
+import org.apache.nifi.parquet.shared.NifiParquetInputFile;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ParquetContentViewerController extends HttpServlet {
+ private static final Logger logger = LoggerFactory.getLogger(ParquetContentViewerController.class);
+ private static final ObjectMapper mapper = new ObjectMapper();
+ private static final long MAX_CONTENT_SIZE = 1024 * 1024 * 2; // 10MB
+ private static final int BUFFER_SIZE = 8 * 1024; // 8KB
+
+ private static final byte NEWLINE = '\n';
+ private static final byte COMMA = ',';
+ private static final byte START_ARRAY = '[';
+ private static final byte END_ARRAY = ']';
+
+ @Override
+ public void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
+ final ContentRequestContext requestContext = new HttpServletContentRequestContext(request);
+
+ final ServletContext servletContext = request.getServletContext();
+ final ContentAccess contentAccess = (ContentAccess) servletContext.getAttribute("nifi-content-access");
+ // get the content
+ final DownloadableContent downloadableContent;
+ try {
+ downloadableContent = contentAccess.getContent(requestContext);
+ } catch (final ResourceNotFoundException e) {
+ logger.warn("Content not found", e);
+ response.sendError(HttpURLConnection.HTTP_NOT_FOUND, "Content not found");
+ return;
+ } catch (final AccessDeniedException e) {
+ logger.warn("Content access denied", e);
+ response.sendError(HttpURLConnection.HTTP_FORBIDDEN, "Content access denied");
+ return;
+ } catch (final Exception e) {
+ logger.warn("Content retrieval failed", e);
+ response.sendError(HttpURLConnection.HTTP_INTERNAL_ERROR, "Content retrieval failed");
+ return;
+ }
+
+ response.setStatus(HttpServletResponse.SC_OK);
+
+ try {
+ // Convert InputStream to a seekable InputStream
+ final byte[] data = getInputStreamBytes(downloadableContent.getContent());
+
+ if (data.length == 0) {
+ response.sendError(HttpURLConnection.HTTP_INTERNAL_ERROR, "Content size is too large to display.");
+ return;
+ }
+
+ final Configuration conf = new Configuration();
+
+ // Allow older deprecated schemas
+ conf.setBoolean("parquet.avro.readInt96AsFixed", true);
+
+ final InputFile inputFile = new NifiParquetInputFile(new ByteArrayInputStream(data), data.length);
+ try (
+ ParquetReader reader = AvroParquetReader.builder(inputFile)
+ .withConf(conf)
+ .build();
+ OutputStream outputStream = response.getOutputStream()) {
+ writeRecords(outputStream, reader);
+ }
+ } catch (final Throwable t) {
+ logger.warn("Unable to format FlowFile content", t);
+ response.sendError(HttpURLConnection.HTTP_INTERNAL_ERROR, "Unable to format FlowFile content");
+ }
+ }
+
+ private void writeRecords(final OutputStream outputStream, final ParquetReader reader) throws IOException {
+ // Format and write out each record
+ GenericRecord record;
+ boolean firstRecord = true;
+ final ObjectWriter objectWriter = mapper.writerWithDefaultPrettyPrinter();
+
+ outputStream.write(START_ARRAY);
+ outputStream.write(NEWLINE);
+ while ((record = reader.read()) != null) {
+ if (firstRecord) {
+ firstRecord = false;
+ } else {
+ outputStream.write(COMMA);
+ outputStream.write(NEWLINE);
+ }
+
+ final Object mappedRecord = recordToMap(record);
+ final byte[] serializedRecord = objectWriter.writeValueAsBytes(mappedRecord);
+ outputStream.write(serializedRecord);
+ }
+ outputStream.write(NEWLINE);
+ outputStream.write(END_ARRAY);
+ }
+
+ private static Object recordToMap(final Object obj) {
+ switch (obj) {
+ case GenericRecord record -> {
+ final Map map = new LinkedHashMap<>();
+ for (final Schema.Field field : record.getSchema().getFields()) {
+ map.put(field.name(), recordToMap(record.get(field.name())));
+ }
+ return map;
+ }
+ case Collection> coll -> {
+ final List