添加了pqdif解析的基础功能,开放了一个基础解析接口。

This commit is contained in:
2026-06-12 10:40:59 +08:00
parent 212b69060c
commit 362bbf536f
6 changed files with 455 additions and 13 deletions

View File

@@ -35,10 +35,41 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId> <artifactId>spring-boot-starter-validation</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>pqdif-native-basic-bridge</artifactId>
<version>1.0.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/pqdif-native-basic-bridge-1.0.0-jar-with-dependencies.jar</systemPath>
</dependency>
</dependencies> </dependencies>
<build> <build>
<finalName>parse-pqdif</finalName> <finalName>parse-pqdif</finalName>
<resources>
<!-- PQDIF native DLL 和示例文件必须原样复制,不能 filtering -->
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>pqdif-native/**</include>
<include>pqdif-samples/**</include>
</includes>
</resource>
<!-- 其他资源按普通方式复制 -->
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>pqdif-native/**</exclude>
<exclude>pqdif-samples/**</exclude>
</excludes>
</resource>
</resources>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

View File

@@ -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);
}
}
}

View File

@@ -4,9 +4,8 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
/** import java.util.List;
* PQDIF 解析占位响应。
*/
@Data @Data
@ApiModel("PQDIF解析响应") @ApiModel("PQDIF解析响应")
public class PqdifParseResponse { public class PqdifParseResponse {
@@ -19,4 +18,75 @@ public class PqdifParseResponse {
@ApiModelProperty("文件名") @ApiModelProperty("文件名")
private String fileName; 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<RecordInfoVO> records;
@ApiModelProperty("Observation 列表")
private List<ObservationVO> 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<ChannelInfoVO> 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<SeriesInfoVO> 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<Double> firstValues;
}
} }

View File

@@ -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<PqdifParseResponse.RecordInfoVO>());
response.setObservations(new ArrayList<PqdifParseResponse.ObservationVO>());
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<PqdifParseResponse.ChannelInfoVO>());
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<PqdifParseResponse.SeriesInfoVO>());
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<Double>());
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<Double>());
}
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<Double> firstValues(double[] values, int maxCount) {
if (values == null || values.length == 0) {
return new ArrayList<Double>();
}
int count = Math.min(values.length, maxCount);
List<Double> result = new ArrayList<Double>();
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();
}
}

View File

@@ -1,24 +1,95 @@
package com.njcn.gather.tool.parsepqdif.service.impl; package com.njcn.gather.tool.parsepqdif.service.impl;
import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifParseResponse; 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 com.njcn.gather.tool.parsepqdif.service.ParsePqdifService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
/** import java.io.InputStream;
* PQDIF 解析服务实现。 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 @Service
public class ParsePqdifServiceImpl implements ParsePqdifService { 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 @Override
public PqdifParseResponse parse(MultipartFile pqdifFile) { 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(); PqdifParseResponse response = new PqdifParseResponse();
response.setStatus(STATUS_NOT_SUPPORTED); response.setStatus(STATUS_FAILED);
response.setMessage("PQDIF解析功能待实现"); response.setMessage(message == null ? "PQDIF解析失败" : message);
response.setFileName(pqdifFile == null ? null : pqdifFile.getOriginalFilename()); response.setFileName(fileName);
response.setRecordCount(0L);
response.setObservationCount(0L);
response.setSampleValueCount(0);
response.setRecords(new ArrayList<PqdifParseResponse.RecordInfoVO>());
response.setObservations(new ArrayList<PqdifParseResponse.ObservationVO>());
return response; return response;
} }
} }