From 362bbf536fca5c83f01fdbb02f78f04d6148d38a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=87?= <3466561528@qq.com> Date: Fri, 12 Jun 2026 10:40:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86pqdif=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E7=9A=84=E5=9F=BA=E7=A1=80=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E5=BC=80=E6=94=BE=E4=BA=86=E4=B8=80=E4=B8=AA=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E6=8E=A5=E5=8F=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/parse-pqdif/pom.xml | 31 +++ .../controller/ParsePqdifController.java | 2 +- .../PqdifNativeLibraryLoader.java | 84 ++++++++ .../pojo/vo/PqdifParseResponse.java | 78 +++++++- .../parsepqdif/reader/PqdifNativeReader.java | 186 ++++++++++++++++++ .../service/impl/ParsePqdifServiceImpl.java | 87 +++++++- 6 files changed, 455 insertions(+), 13 deletions(-) create mode 100644 tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/nativebridge/PqdifNativeLibraryLoader.java create mode 100644 tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/reader/PqdifNativeReader.java diff --git a/tools/parse-pqdif/pom.xml b/tools/parse-pqdif/pom.xml index 9a6d660..22e177e 100644 --- a/tools/parse-pqdif/pom.xml +++ b/tools/parse-pqdif/pom.xml @@ -35,10 +35,41 @@ org.springframework.boot spring-boot-starter-validation + + + com.njcn + pqdif-native-basic-bridge + 1.0.0 + system + ${project.basedir}/lib/pqdif-native-basic-bridge-1.0.0-jar-with-dependencies.jar + parse-pqdif + + + + + src/main/resources + false + + pqdif-native/** + pqdif-samples/** + + + + + + src/main/resources + false + + pqdif-native/** + pqdif-samples/** + + + + org.apache.maven.plugins diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/ParsePqdifController.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/ParsePqdifController.java index 58f7320..997b494 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/ParsePqdifController.java +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/ParsePqdifController.java @@ -28,7 +28,7 @@ import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/api/parse-pqdif") @RequiredArgsConstructor -public class ParsePqdifController extends BaseController { +public class ParsePqdifController extends BaseController { private final ParsePqdifService parsePqdifService; diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/nativebridge/PqdifNativeLibraryLoader.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/nativebridge/PqdifNativeLibraryLoader.java new file mode 100644 index 0000000..a40bf95 --- /dev/null +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/nativebridge/PqdifNativeLibraryLoader.java @@ -0,0 +1,84 @@ +package com.njcn.gather.tool.parsepqdif.nativebridge; + +import com.sun.jna.NativeLibrary; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.*; +import java.util.Locale; + +public final class PqdifNativeLibraryLoader { + + private static final String RESOURCE_DLL = "/pqdif-native/win-x64/pqdifbasic.dll"; + private static final String DLL_NAME = "pqdifbasic.dll"; + private static final String TEMP_DIR_NAME = "cn-tool-pqdif-native"; + + private static volatile boolean prepared; + private static Path preparedNativeDir; + + private PqdifNativeLibraryLoader() { + } + + public static synchronized Path ensurePrepared() { + if (prepared) { + return preparedNativeDir; + } + + if (!isWindows()) { + throw new IllegalStateException("当前接入的是 Windows x64 版 pqdifbasic.dll,非 Windows 环境无法加载"); + } + + try { + Path nativeDir = Paths.get(System.getProperty("java.io.tmpdir"), TEMP_DIR_NAME, "win-x64"); + Files.createDirectories(nativeDir); + + Path dllPath = nativeDir.resolve(DLL_NAME); + copyResourceDll(dllPath); + + appendLibraryPath("jna.library.path", nativeDir); + appendLibraryPath("java.library.path", nativeDir); + + NativeLibrary.addSearchPath("pqdifbasic", nativeDir.toAbsolutePath().toString()); + NativeLibrary.addSearchPath("pqdifbasic.dll", nativeDir.toAbsolutePath().toString()); + + System.out.println("PQDIF native dir = " + nativeDir.toAbsolutePath()); + System.out.println("PQDIF native dll = " + dllPath.toAbsolutePath()); + + preparedNativeDir = nativeDir; + prepared = true; + return nativeDir; + } catch (IOException e) { + throw new IllegalStateException("准备 pqdifbasic.dll 失败:" + e.getMessage(), e); + } + } + + private static boolean isWindows() { + String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + return osName.contains("win"); + } + + private static void copyResourceDll(Path dllPath) throws IOException { + try (InputStream inputStream = PqdifNativeLibraryLoader.class.getResourceAsStream(RESOURCE_DLL)) { + if (inputStream == null) { + throw new IOException("classpath 中找不到 " + RESOURCE_DLL); + } + + Files.copy(inputStream, dllPath, StandardCopyOption.REPLACE_EXISTING); + } + } + + private static void appendLibraryPath(String propertyName, Path nativeDir) { + String nativePath = nativeDir.toAbsolutePath().toString(); + String oldValue = System.getProperty(propertyName); + String separator = System.getProperty("path.separator"); + + if (oldValue == null || oldValue.trim().isEmpty()) { + System.setProperty(propertyName, nativePath); + return; + } + + if (!oldValue.toLowerCase(Locale.ROOT).contains(nativePath.toLowerCase(Locale.ROOT))) { + System.setProperty(propertyName, nativePath + separator + oldValue); + } + } +} \ No newline at end of file diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/PqdifParseResponse.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/PqdifParseResponse.java index 313f56a..40d047a 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/PqdifParseResponse.java +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/PqdifParseResponse.java @@ -4,9 +4,8 @@ import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; -/** - * PQDIF 解析占位响应。 - */ +import java.util.List; + @Data @ApiModel("PQDIF解析响应") public class PqdifParseResponse { @@ -19,4 +18,75 @@ public class PqdifParseResponse { @ApiModelProperty("文件名") private String fileName; -} + + @ApiModelProperty("native 解析库版本") + private String nativeVersion; + + @ApiModelProperty("Record 总数") + private Long recordCount; + + @ApiModelProperty("Observation Record 总数") + private Long observationCount; + + @ApiModelProperty("每个 Series 返回的样例值数量") + private Integer sampleValueCount; + + @ApiModelProperty("Record 列表") + private List records; + + @ApiModelProperty("Observation 列表") + private List observations; + + @Data + public static class RecordInfoVO { + private Long recordIndex; + private String typeGuid; + private String typeName; + private Boolean observation; + } + + @Data + public static class ObservationVO { + private Long recordIndex; + private String name; + private Double timeStartExcelDays; + private String timeStartText; + private Long channelCount; + private List channels; + } + + @Data + public static class ChannelInfoVO { + private Long channelIndex; + private String name; + private Long seriesCount; + private Long phaseId; + private String quantityTypeGuid; + private Long quantityMeasuredId; + private List series; + } + + @Data + public static class SeriesInfoVO { + private Long seriesIndex; + private Long quantityUnitsId; + private String quantityCharacteristicGuid; + private String valueTypeGuid; + private Long seriesBaseType; + private Double scale; + private Double offset; + + /** + * DATA_SUCCESS / DATA_FAILED + */ + private String dataStatus; + + /** + * 数据读取失败原因 + */ + private String dataMessage; + + private Integer valueCount; + private List firstValues; + } +} \ No newline at end of file diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/reader/PqdifNativeReader.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/reader/PqdifNativeReader.java new file mode 100644 index 0000000..58061d6 --- /dev/null +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/reader/PqdifNativeReader.java @@ -0,0 +1,186 @@ +package com.njcn.gather.tool.parsepqdif.reader; + +import com.njcn.gather.tool.parsepqdif.nativebridge.PqdifNativeLibraryLoader; +import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifParseResponse; +import com.njcn.pqdif.nativebridge.PqdifBasicNative; +import com.njcn.pqdif.nativebridge.PqdifNativeSession; + +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.*; +import java.util.ArrayList; + +public class PqdifNativeReader { + + private static final String STATUS_SUCCESS = "SUCCESS"; + private static final int DEFAULT_SAMPLE_VALUE_COUNT = 5; + + public PqdifParseResponse read(Path pqdifPath, String fileName) { + PqdifNativeLibraryLoader.ensurePrepared(); + + PqdifParseResponse response = new PqdifParseResponse(); + response.setStatus(STATUS_SUCCESS); + response.setMessage("PQDIF解析完成"); + response.setFileName(fileName); + response.setNativeVersion(PqdifBasicNative.INSTANCE.pqdif_basic_version()); + response.setSampleValueCount(DEFAULT_SAMPLE_VALUE_COUNT); + response.setRecords(new ArrayList()); + response.setObservations(new ArrayList()); + + try (PqdifNativeSession session = new PqdifNativeSession()) { + session.open(pqdifPath.toAbsolutePath().toString()); + + long recordCount = session.getRecordCount(); + response.setRecordCount(recordCount); + + for (long recordIndex = 0; recordIndex < recordCount; recordIndex++) { + PqdifNativeSession.RecordInfo recordInfo = session.getRecordInfo(recordIndex); + + PqdifParseResponse.RecordInfoVO recordVO = new PqdifParseResponse.RecordInfoVO(); + recordVO.setRecordIndex(recordIndex); + recordVO.setTypeGuid(recordInfo.typeGuid); + recordVO.setTypeName(recordInfo.typeName); + recordVO.setObservation(isObservation(recordInfo)); + response.getRecords().add(recordVO); + + if (!isObservation(recordInfo)) { + continue; + } + + try (PqdifNativeSession.Observation observation = session.openObservation(recordIndex)) { + response.getObservations().add(readObservation(recordIndex, observation)); + } + } + } + + response.setObservationCount((long) response.getObservations().size()); + return response; + } + + private PqdifParseResponse.ObservationVO readObservation( + long recordIndex, + PqdifNativeSession.Observation observation) { + + PqdifNativeSession.ObservationInfo info = observation.getInfo(); + + PqdifParseResponse.ObservationVO vo = new PqdifParseResponse.ObservationVO(); + vo.setRecordIndex(recordIndex); + vo.setName(info.name); + vo.setTimeStartExcelDays(info.timeStartExcelDays); + vo.setTimeStartText(toExcelDateTimeText(info.timeStartExcelDays)); + vo.setChannelCount(info.channelCount); + vo.setChannels(new ArrayList()); + + for (long channelIndex = 0; channelIndex < info.channelCount; channelIndex++) { + PqdifNativeSession.ChannelInfo channelInfo = observation.getChannelInfo(channelIndex); + vo.getChannels().add(readChannel(observation, channelIndex, channelInfo)); + } + + return vo; + } + + private PqdifParseResponse.ChannelInfoVO readChannel( + PqdifNativeSession.Observation observation, + long channelIndex, + PqdifNativeSession.ChannelInfo channelInfo) { + + PqdifParseResponse.ChannelInfoVO vo = new PqdifParseResponse.ChannelInfoVO(); + vo.setChannelIndex(channelIndex); + vo.setName(channelInfo.name); + vo.setSeriesCount(channelInfo.seriesCount); + vo.setPhaseId(channelInfo.phaseId); + vo.setQuantityTypeGuid(channelInfo.quantityTypeGuid); + vo.setQuantityMeasuredId(channelInfo.quantityMeasuredId); + vo.setSeries(new ArrayList()); + + for (long seriesIndex = 0; seriesIndex < channelInfo.seriesCount; seriesIndex++) { + vo.getSeries().add(readSeries(observation, channelIndex, seriesIndex)); + } + + return vo; + } + + private PqdifParseResponse.SeriesInfoVO readSeries( + PqdifNativeSession.Observation observation, + long channelIndex, + long seriesIndex) { + + PqdifParseResponse.SeriesInfoVO vo = new PqdifParseResponse.SeriesInfoVO(); + vo.setSeriesIndex(seriesIndex); + + PqdifNativeSession.SeriesInfo seriesInfo = null; + + try { + seriesInfo = observation.getSeriesInfo(channelIndex, seriesIndex); + + vo.setQuantityUnitsId(seriesInfo.quantityUnitsId); + vo.setQuantityCharacteristicGuid(seriesInfo.quantityCharacteristicGuid); + vo.setValueTypeGuid(seriesInfo.valueTypeGuid); + vo.setSeriesBaseType(seriesInfo.seriesBaseType); + vo.setScale(seriesInfo.scale); + vo.setOffset(seriesInfo.offset); + } catch (Throwable e) { + vo.setDataStatus("DATA_FAILED"); + vo.setDataMessage("getSeriesInfo failed, channel=" + channelIndex + + ", series=" + seriesIndex + + ", error=" + e.getMessage()); + vo.setValueCount(0); + vo.setFirstValues(new ArrayList()); + return vo; + } + + try { + double[] values = observation.getSeriesData(channelIndex, seriesIndex); + + vo.setDataStatus("DATA_SUCCESS"); + vo.setDataMessage(null); + vo.setValueCount(values == null ? 0 : values.length); + vo.setFirstValues(firstValues(values, DEFAULT_SAMPLE_VALUE_COUNT)); + } catch (Throwable e) { + vo.setDataStatus("DATA_FAILED"); + vo.setDataMessage("getSeriesData failed, channel=" + channelIndex + + ", series=" + seriesIndex + + ", seriesBaseType=" + vo.getSeriesBaseType() + + ", valueTypeGuid=" + vo.getValueTypeGuid() + + ", characteristicGuid=" + vo.getQuantityCharacteristicGuid() + + ", error=" + e.getMessage()); + vo.setValueCount(0); + vo.setFirstValues(new ArrayList()); + } + + return vo; + } + + private static boolean isObservation(PqdifNativeSession.RecordInfo recordInfo) { + if (recordInfo == null || recordInfo.typeName == null) { + return false; + } + String type = recordInfo.typeName.toLowerCase(Locale.ROOT); + return type.contains("observation") || type.contains("tagrecobservation"); + } + + private static List firstValues(double[] values, int maxCount) { + if (values == null || values.length == 0) { + return new ArrayList(); + } + + int count = Math.min(values.length, maxCount); + List result = new ArrayList(); + for (int i = 0; i < count; i++) { + result.add(values[i]); + } + return result; + } + + private static String toExcelDateTimeText(double excelDays) { + long days = (long) Math.floor(excelDays); + double dayFraction = excelDays - days; + long nanos = Math.round(dayFraction * 24D * 60D * 60D * 1_000_000_000D); + + LocalDateTime dateTime = LocalDateTime.of(1899, 12, 30, 0, 0) + .plusDays(days) + .plusNanos(nanos); + + return dateTime.toString(); + } +} \ No newline at end of file diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/ParsePqdifServiceImpl.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/ParsePqdifServiceImpl.java index 78b9fe4..3bd0d3d 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/ParsePqdifServiceImpl.java +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/ParsePqdifServiceImpl.java @@ -1,24 +1,95 @@ package com.njcn.gather.tool.parsepqdif.service.impl; import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifParseResponse; +import com.njcn.gather.tool.parsepqdif.reader.PqdifNativeReader; import com.njcn.gather.tool.parsepqdif.service.ParsePqdifService; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -/** - * PQDIF 解析服务实现。 - */ +import java.io.InputStream; +import java.nio.file.*; +import java.util.ArrayList; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +@Slf4j @Service public class ParsePqdifServiceImpl implements ParsePqdifService { - private static final String STATUS_NOT_SUPPORTED = "NOT_SUPPORTED"; + private static final String STATUS_FAILED = "FAILED"; + + private final PqdifNativeReader pqdifNativeReader = new PqdifNativeReader(); @Override public PqdifParseResponse parse(MultipartFile pqdifFile) { + if (pqdifFile == null || pqdifFile.isEmpty()) { + return failed(null, "PQDIF文件不能为空"); + } + + Path tempFile = null; + + try { + tempFile = createTempPqdifFile(pqdifFile); + return pqdifNativeReader.read(tempFile, pqdifFile.getOriginalFilename()); + } catch (Throwable e) { + log.error("PQDIF解析失败,fileName={}", pqdifFile.getOriginalFilename(), e); + return failed(pqdifFile.getOriginalFilename(), e.getMessage()); + } finally { + if (tempFile != null) { + try { + Files.deleteIfExists(tempFile); + } catch (Exception e) { + log.warn("删除PQDIF临时文件失败,path={}", tempFile, e); + } + } + } + } + + private Path createTempPqdifFile(MultipartFile pqdifFile) throws Exception { + String originalFilename = pqdifFile.getOriginalFilename(); + String suffix = getSuffix(originalFilename); + + Path uploadDir = Paths.get("D:", "CN_Tool_Runtime", "pqdif-upload"); + Files.createDirectories(uploadDir); + + String safeFileName = "parse-pqdif-" + System.currentTimeMillis() + "-" + + java.util.UUID.randomUUID().toString().replace("-", "") + suffix; + + Path tempFile = uploadDir.resolve(safeFileName); + + try (InputStream inputStream = pqdifFile.getInputStream()) { + Files.copy(inputStream, tempFile, StandardCopyOption.REPLACE_EXISTING); + } + + return tempFile; + } + + private String getSuffix(String originalFilename) { + if (originalFilename == null) { + return ".pqd"; + } + + int index = originalFilename.lastIndexOf('.'); + if (index < 0 || index == originalFilename.length() - 1) { + return ".pqd"; + } + + return originalFilename.substring(index); + } + + private PqdifParseResponse failed(String fileName, String message) { PqdifParseResponse response = new PqdifParseResponse(); - response.setStatus(STATUS_NOT_SUPPORTED); - response.setMessage("PQDIF解析功能待实现"); - response.setFileName(pqdifFile == null ? null : pqdifFile.getOriginalFilename()); + response.setStatus(STATUS_FAILED); + response.setMessage(message == null ? "PQDIF解析失败" : message); + response.setFileName(fileName); + response.setRecordCount(0L); + response.setObservationCount(0L); + response.setSampleValueCount(0); + response.setRecords(new ArrayList()); + response.setObservations(new ArrayList()); return response; } -} +} \ No newline at end of file